feat: connect alpine agents to admin api

This commit is contained in:
a.tolmachev
2026-03-30 01:31:07 +03:00
parent 85395b10f8
commit e8a4a1138b
9 changed files with 807 additions and 220 deletions
+219 -5
View File
@@ -118,6 +118,13 @@ pub struct AgentPayload {
pub tool_selection_policy: Value,
}
#[derive(Clone, Debug, Deserialize)]
pub struct UpdateAgentPayload {
pub slug: String,
pub display_name: String,
pub description: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AgentBindingPayload {
pub operation_id: String,
@@ -145,6 +152,33 @@ pub struct PublishAgentResponse {
pub published_at: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct AgentSummaryView {
pub id: String,
pub workspace_id: String,
pub slug: String,
pub display_name: String,
pub description: String,
pub status: AgentStatus,
pub current_draft_version: u32,
pub latest_published_version: Option<u32>,
pub created_at: String,
pub updated_at: String,
pub published_at: Option<String>,
pub operation_count: usize,
pub operation_ids: Vec<String>,
pub key_count: usize,
pub calls_today: u64,
pub mcp_endpoint: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct AgentMutationResult {
pub agent_id: String,
pub workspace_id: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct InvitationPayload {
pub email: String,
@@ -1170,9 +1204,54 @@ impl AdminService {
pub async fn list_agents(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<AgentSummary>, ApiError> {
) -> Result<Vec<AgentSummaryView>, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
Ok(self.registry.list_agents(workspace_id).await?)
let workspace = self.get_workspace(workspace_id).await?;
let summaries = self.registry.list_agents(workspace_id).await?;
let usage = self
.registry
.list_usage_by_agent(UsageQuery {
workspace_id,
period: UsagePeriod::Last24Hours,
source: None,
created_after: &today_start_utc()?,
bucket: crank_registry::UsageBucket::Hour,
})
.await?;
let calls_today = usage
.into_iter()
.map(|item| (item.agent_id.as_str().to_owned(), item.calls_total))
.collect::<BTreeMap<_, _>>();
let key_count = self
.registry
.list_platform_api_keys(workspace_id)
.await?
.len();
let mut items = Vec::with_capacity(summaries.len());
for summary in summaries {
let version = self
.get_agent_version(workspace_id, &summary.id, summary.current_draft_version)
.await?;
let operation_ids = version
.bindings
.iter()
.map(|binding| binding.operation_id.as_str().to_owned())
.collect::<Vec<_>>();
items.push(AgentSummaryView {
operation_count: operation_ids.len(),
operation_ids,
key_count,
calls_today: calls_today.get(summary.id.as_str()).copied().unwrap_or(0),
mcp_endpoint: agent_mcp_endpoint(
workspace.workspace.slug.as_str(),
summary.slug.as_str(),
),
..map_agent_summary_view(summary)
});
}
Ok(items)
}
#[instrument(skip(self))]
@@ -1180,13 +1259,54 @@ impl AdminService {
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
) -> Result<AgentSummary, ApiError> {
self.registry
) -> Result<AgentSummaryView, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
let workspace = self.get_workspace(workspace_id).await?;
let summary = self
.registry
.get_agent_summary(workspace_id, agent_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!("agent {} was not found", agent_id.as_str()))
})
})?;
let version = self
.get_agent_version(workspace_id, agent_id, summary.current_draft_version)
.await?;
let operation_ids = version
.bindings
.iter()
.map(|binding| binding.operation_id.as_str().to_owned())
.collect::<Vec<_>>();
let usage = self
.registry
.get_usage_for_agent(
UsageQuery {
workspace_id,
period: UsagePeriod::Last24Hours,
source: None,
created_after: &today_start_utc()?,
bucket: crank_registry::UsageBucket::Hour,
},
agent_id,
)
.await?;
let key_count = self
.registry
.list_platform_api_keys(workspace_id)
.await?
.len();
Ok(AgentSummaryView {
operation_count: operation_ids.len(),
operation_ids,
key_count,
calls_today: usage.map(|item| item.rollup.calls_total).unwrap_or(0),
mcp_endpoint: agent_mcp_endpoint(
workspace.workspace.slug.as_str(),
summary.slug.as_str(),
),
..map_agent_summary_view(summary)
})
}
#[instrument(skip(self))]
@@ -1267,6 +1387,75 @@ impl AdminService {
})
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str()))]
pub async fn update_agent(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
payload: UpdateAgentPayload,
) -> Result<AgentMutationResult, ApiError> {
let existing = self
.registry
.get_agent_summary(workspace_id, agent_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!("agent {} was not found", agent_id.as_str()))
})?;
if payload.slug != existing.slug
&& self
.find_agent_by_slug(workspace_id, &payload.slug)
.await?
.is_some()
{
return Err(ApiError::conflict(format!(
"agent with slug {} already exists",
payload.slug
)));
}
let updated_at = now_string()?;
self.registry
.update_agent_summary(
workspace_id,
agent_id,
&payload.slug,
&payload.display_name,
&payload.description,
&updated_at,
)
.await?;
Ok(AgentMutationResult {
agent_id: agent_id.as_str().to_owned(),
workspace_id: workspace_id.as_str().to_owned(),
updated_at,
})
}
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str()))]
pub async fn delete_agent(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
) -> Result<AgentMutationResult, ApiError> {
let existing = self
.registry
.get_agent_summary(workspace_id, agent_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!("agent {} was not found", agent_id.as_str()))
})?;
self.registry.delete_agent(workspace_id, agent_id).await?;
Ok(AgentMutationResult {
agent_id: agent_id.as_str().to_owned(),
workspace_id: workspace_id.as_str().to_owned(),
updated_at: existing.updated_at,
})
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str()))]
pub async fn save_agent_bindings(
&self,
@@ -2060,6 +2249,31 @@ fn agent_ref_map(items: Vec<OperationAgentRef>) -> BTreeMap<String, Vec<Operatio
map
}
fn map_agent_summary_view(summary: AgentSummary) -> AgentSummaryView {
AgentSummaryView {
id: summary.id.as_str().to_owned(),
workspace_id: summary.workspace_id.as_str().to_owned(),
slug: summary.slug,
display_name: summary.display_name,
description: summary.description,
status: summary.status,
current_draft_version: summary.current_draft_version,
latest_published_version: summary.latest_published_version,
created_at: summary.created_at,
updated_at: summary.updated_at,
published_at: summary.published_at,
operation_count: 0,
operation_ids: Vec::new(),
key_count: 0,
calls_today: 0,
mcp_endpoint: String::new(),
}
}
fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
format!("/mcp/v1/{workspace_slug}/{agent_slug}")
}
fn enrich_operation_summary(
summary: OperationSummary,
usage_summary: OperationUsageSummaryView,