Large World graph, agent platform, brain stack & dashboard rebuild
Frontend - Large World: collapse org/company/team tiers into one expandable React Flow hierarchy (WorldFlow) with per-click expand, persisted node positions, a compact tree sidebar, wrench multi-select delete across levels, and a sized right slide-out (phone/tablet/full) showing an agent summary + drill button. - Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible System Prompt + Personality cards, restructured anatomy cards, bigger avatar with name/title header row, Markdown/JSON-aware rendering, brain registry + history, avatar generate/upload. - User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel; Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered); Team Runs view; reap-progress modal; dashboard is the single live interface. Backend - cm-brain crate (.brain as the agent definition) + brain apply/history. - Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete. - Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks (migration 0013), org/company/team delete endpoints, scheduler sweeps. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9f266d5806
commit
34f744734b
@@ -0,0 +1,122 @@
|
||||
//! Inbound webhook triggers (Triggered deploy mode). A team gets a token whose
|
||||
//! public URL, when POSTed, enqueues the team's topology run — reusing the same
|
||||
//! durable-run path as the scheduler and the UI "Run team" button.
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
/// `POST /api/teams/{id}/webhooks` — mint a webhook token for the team.
|
||||
pub async fn create_webhook(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(team_id): Path<Uuid>,
|
||||
body: Option<Json<CreateBody>>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
// Scope check: the team must belong to the caller's workspace.
|
||||
cm_db::repo::teams::get_team(&state.pool, team_id, user.workspace_id).await?;
|
||||
let default_task = body.map(|b| b.0.task).unwrap_or_default();
|
||||
let id = Uuid::now_v7();
|
||||
let token = Uuid::now_v7();
|
||||
sqlx::query("INSERT INTO webhook_tokens (id, workspace_id, team_id, token, task) VALUES ($1, $2, $3, $4, $5)")
|
||||
.bind(id)
|
||||
.bind(user.workspace_id.as_uuid())
|
||||
.bind(team_id)
|
||||
.bind(token)
|
||||
.bind(&default_task)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal)?;
|
||||
Ok(Json(json!({ "token": token.to_string(), "url": format!("/api/hooks/{token}") })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct CreateBody {
|
||||
#[serde(default)]
|
||||
pub task: String,
|
||||
}
|
||||
|
||||
/// `GET /api/teams/{id}/webhooks` — list the team's webhook tokens.
|
||||
pub async fn list_webhooks(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(team_id): Path<Uuid>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
cm_db::repo::teams::get_team(&state.pool, team_id, user.workspace_id).await?;
|
||||
let rows = sqlx::query_as::<_, (Uuid,)>(
|
||||
"SELECT token FROM webhook_tokens WHERE team_id = $1 ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal)?;
|
||||
let hooks: Vec<Value> = rows
|
||||
.into_iter()
|
||||
.map(|(t,)| json!({ "token": t.to_string(), "url": format!("/api/hooks/{t}") }))
|
||||
.collect();
|
||||
Ok(Json(json!({ "webhooks": hooks })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct HookBody {
|
||||
#[serde(default)]
|
||||
task: String,
|
||||
}
|
||||
|
||||
/// `POST /api/hooks/{token}` — PUBLIC. Fire the bound team's topology run. Body
|
||||
/// `{task?}` overrides the team's default task. Mirrors the Slack/Stripe inbound
|
||||
/// pattern (no session auth; the unguessable token is the credential).
|
||||
pub async fn trigger_hook(
|
||||
State(state): State<AppState>,
|
||||
Path(token): Path<Uuid>,
|
||||
body: axum::body::Bytes,
|
||||
) -> StatusCode {
|
||||
let row = sqlx::query_as::<_, (Uuid, Uuid, String)>(
|
||||
"SELECT workspace_id, team_id, task FROM webhook_tokens WHERE token = $1",
|
||||
)
|
||||
.bind(token)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((ws_id, team_id, default_task)) = row else {
|
||||
return StatusCode::NOT_FOUND;
|
||||
};
|
||||
let ws = cm_domain::WorkspaceId::from(ws_id);
|
||||
let Ok(team) = cm_db::repo::teams::get_team(&state.pool, team_id, ws).await else {
|
||||
return StatusCode::NOT_FOUND;
|
||||
};
|
||||
let task = serde_json::from_slice::<HookBody>(&body)
|
||||
.ok()
|
||||
.map(|b| b.task)
|
||||
.filter(|t| !t.trim().is_empty())
|
||||
.or_else(|| (!default_task.trim().is_empty()).then_some(default_task))
|
||||
.unwrap_or_else(|| "webhook trigger".to_string());
|
||||
let run_id = Uuid::now_v7();
|
||||
if cm_db::repo::topology_runs::enqueue_run(&state.pool, run_id, ws, &task, &team.graph)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
let _ = sqlx::query("UPDATE webhook_tokens SET last_fired_at = now() WHERE token = $1")
|
||||
.bind(token)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
let _ = cm_db::repo::audit::append(
|
||||
&state.pool,
|
||||
ws,
|
||||
cm_db::repo::audit::Actor::System,
|
||||
"topology.webhook_triggered",
|
||||
"webhook",
|
||||
&token.to_string(),
|
||||
json!({ "team_id": team_id, "run_id": run_id }),
|
||||
)
|
||||
.await;
|
||||
StatusCode::ACCEPTED
|
||||
}
|
||||
Reference in New Issue
Block a user