//! REST API for Clawmates (spec §13). One route resource per module. pub mod agent_lifecycle; pub mod agent_names; pub mod auto_merge; pub mod benchmark_runner; pub mod beszel; pub mod brain_seed; pub mod cleanup_sweeper; pub mod container_exec; pub mod corpus; mod error; pub mod evaluator; pub mod evaluator_tools; mod extract; pub mod fleet; pub mod fleet_herdr; pub mod harvest; pub mod level_up; pub mod library; pub mod live_bus; mod mcp_door; mod mcp_skills; pub mod microvm_client; pub mod microvm_executor; pub mod microvm_turn_executor; pub mod continuous_research; pub mod mission_delivery; pub mod podcast; pub mod mission_events; pub mod mission_fs; pub mod mission_gc; pub mod mission_orchestrator; pub mod mission_schedule; pub mod mission_outputs; pub mod mission_plan; pub mod mission_refiner; pub mod mission_roster; pub mod mission_runtime; pub mod mission_workspace; pub mod node_rules; pub mod papers; pub mod phase_config; pub mod phase_runner; pub mod phase_summarizer; pub mod quota; mod recursive_exec; pub mod repo_digest; pub mod root_copy; mod routes; pub mod runtime_preflight; pub mod runtime_provision; pub mod security_scan; pub mod session_executor; pub mod container_tool_hooks; pub mod gateway_preflight; pub mod skill_delivery; pub mod skill_self_authoring; pub mod skill_use; pub mod skills_loader; pub mod subscription; pub mod swarm; pub mod task_card_parser; pub mod task_card_worker; pub mod team_template_loader; pub mod tool_versions; pub mod topology_exec; pub mod topology_worker; pub mod validator_preflight; pub mod vm_placement; pub mod vm_stop_gate; pub mod vm_tool_gate; pub mod vm_tool_tap; pub mod workflow_registry; use axum::routing::{delete, get, patch, post}; use axum::Router; use cm_auth::AuthService; use cm_runtime::Runtime; use sqlx::PgPool; pub use error::ApiError; pub use extract::Authed; #[derive(Clone)] pub struct AppState { pub pool: PgPool, pub auth: AuthService, pub runtime: Runtime, /// Secret broker socket; connect flows refuse without it. pub broker_socket: Option, pub oauth: cm_config::OAuthConfig, pub billing: cm_config::BillingConfig, /// Short-lived single-use tickets for the Terminal WebSocket. /// Local blob-store root (Some on the Local backend) so the Files app can /// reconcile its index with files the Terminal wrote into the drives. pub file_root: Option, /// Live control channels to connected fleet-node daemons. pub node_hub: std::sync::Arc, /// The shelf. Present once the server wires storage; `None` in the /// bare-`new` path used by tests that never touch blobs. pub blobs: Option>, } impl AppState { pub fn new(pool: PgPool, runtime: Runtime) -> AppState { let auth = AuthService::new(pool.clone()); AppState { pool, auth, runtime, broker_socket: None, oauth: cm_config::OAuthConfig::default(), billing: cm_config::BillingConfig::default(), file_root: None, node_hub: std::sync::Arc::new(fleet::NodeHub::new()), blobs: None, } } pub fn with_file_root(mut self, root: Option) -> AppState { self.file_root = root; self } /// The shelf — where the paper library stores PDFs. pub fn with_blobs(mut self, blobs: std::sync::Arc) -> AppState { self.blobs = Some(blobs); self } pub fn with_oauth(mut self, oauth: cm_config::OAuthConfig) -> AppState { self.oauth = oauth; self } /// SaaS: give each new hosted-identity sign-in its own workspace. pub fn with_per_signup_workspace(mut self, enabled: bool) -> AppState { self.auth = self.auth.with_per_signup_workspace(enabled); self } pub fn with_billing(mut self, billing: cm_config::BillingConfig) -> AppState { self.billing = billing; self } /// Optional form of [`AppState::with_auth_verifier`] for call chains. pub fn pipe_auth_verifier( self, verifier: Option>, ) -> AppState { match verifier { Some(verifier) => self.with_auth_verifier(verifier), None => self, } } /// Hosted-identity session JWTs (Clerk / OIDC) for the Authed /// extractor, alongside local sessions. pub fn with_auth_verifier( mut self, verifier: std::sync::Arc, ) -> AppState { self.auth = self.auth.with_verifier(verifier); self } pub fn with_broker(mut self, socket: std::path::PathBuf) -> AppState { self.broker_socket = Some(socket); self } /// Share a pre-built node hub (so the fleet driver provider and the routes /// use the same live channels). pub fn with_node_hub(mut self, hub: std::sync::Arc) -> AppState { self.node_hub = hub; self } } pub fn router(state: AppState) -> Router { Router::new() .route("/healthz", get(routes::health::healthz)) .route("/api/quota", get(quota::get_quota)) .route("/api/world/live", get(routes::world::world_live)) .route("/api/world/replay", get(routes::world::world_replay)) .route("/api/nodes", get(routes::nodes::list)) .route("/api/fleet/capacity", get(routes::nodes::capacity)) .route("/api/fleet/backends", get(routes::nodes::backends)) .route("/api/nodes/pair", post(routes::nodes::pair)) .route("/api/nodes/live", get(routes::nodes::live)) .route("/api/nodes/agent", get(routes::nodes::agent_ws)) .route("/api/nodes/{id}/exec-test", post(routes::nodes::exec_test)) .route( "/api/nodes/{id}/sandbox-check", post(routes::nodes::sandbox_check), ) .route( "/api/nodes/{id}/terminal/ticket", post(routes::nodes::terminal_ticket), ) .route( "/api/nodes/{id}/terminal/ws", get(routes::nodes::terminal_ws), ) .route( "/api/nodes/{id}/metrics", get(routes::beszel::node_metrics_get), ) .route("/api/nodes/{id}/tools", get(routes::nodes::tools)) .route( "/api/nodes/{id}/tools/{tool}/update", post(routes::nodes::tool_update), ) .route( "/api/nodes/{id}/herdr/session", get(routes::nodes::herdr_session), ) .route("/api/nodes/{id}", delete(routes::nodes::remove)) .route( "/api/fleet/beszel", get(routes::beszel::status) .post(routes::beszel::connect) .delete(routes::beszel::disconnect), ) .route( "/api/fleet/rules", get(routes::beszel::rules_list).post(routes::beszel::rules_create), ) .route( "/api/fleet/rules/{id}", patch(routes::beszel::rules_patch).delete(routes::beszel::rules_delete), ) .route( "/api/fleet/tailscale", get(routes::tailscale::status) .post(routes::tailscale::connect) .delete(routes::tailscale::disconnect), ) .route( "/api/fleet/tailscale/devices", get(routes::tailscale::devices), ) .route( "/api/fleet/placement", get(routes::tailscale::placement_get).put(routes::tailscale::placement_set), ) .route("/mcp", post(mcp_door::mcp)) .route("/mcp/skills", post(mcp_skills::mcp_skills)) .route("/api/auth/login", post(routes::auth::login)) .route("/api/auth/logout", post(routes::auth::logout)) .route("/api/user/me", get(routes::identity::me)) .route("/api/claws", post(routes::claws::create)) .route("/api/claws/batch-delete", post(routes::claws::batch_delete)) .route("/api/claws/lifecycle", get(routes::claws::lifecycle_census)) .route( "/api/claws/lifecycle/sweep", post(routes::claws::lifecycle_sweep), ) .route("/api/claws/{id}", patch(routes::claws::patch)) .route("/api/claws/{id}", delete(routes::claws::delete)) .route("/api/claws/{id}/model", patch(routes::claws::set_model)) .route( "/api/claws/{id}/access", axum::routing::put(routes::claws::set_access), ) .route( "/api/claws/{id}/runtime-config", get(routes::claws::runtime_config), ) .route( "/api/claws/{id}/compartments", get(routes::claws::compartments), ) .route( "/api/claws/{id}/brain", get(routes::claws::brain).patch(routes::claws::edit_brain), ) .route( "/api/claws/{id}/brain/push", axum::routing::post(routes::claws::push_brain), ) .route( "/api/brainhub/pull", axum::routing::post(routes::claws::pull_brain), ) .route("/api/brainhub/search", get(routes::claws::brainhub_search)) .route( "/api/brainhub/preview", get(routes::claws::brainhub_preview), ) .route( "/api/brainhub/enhance", axum::routing::post(routes::claws::enhance_brain), ) .route( "/api/claws/{id}/brain/revisions", axum::routing::get(routes::claws::brain_revisions), ) .route( "/api/claws/{id}/brain/rollback", axum::routing::post(routes::claws::brain_rollback), ) .route( "/api/planner/chat", axum::routing::post(routes::planner::planner_chat), ) .route( "/api/planner/scaffold", axum::routing::post(routes::planner::planner_scaffold), ) .route( "/api/claws/{id}/brain/apply", axum::routing::post(routes::claws::apply_brain), ) .route("/api/terminal/{id}/ticket", post(routes::terminal::ticket)) .route( "/api/terminal/{id}/tabs", get(routes::terminal::get_tabs).put(routes::terminal::save_tabs), ) .route("/api/terminal/{id}/ws", get(routes::terminal::ws)) .route("/api/claw-chat/threads", get(routes::claw_chat::threads)) .route("/api/claw-chat/messages", get(routes::claw_chat::messages)) .route( "/api/claw-chat/rooms", get(routes::claw_chat::rooms).post(routes::claw_chat::create_room), ) .route( "/api/claw-chat/rooms/{threadId}/participants", post(routes::claw_chat::add_participant), ) .route( "/api/claw-chat/rooms/{threadId}/participants/{clawId}", delete(routes::claw_chat::remove_participant), ) // A2A: operator settings/tokens (session-authed) + public ingress. .route( "/api/a2a/settings", get(routes::a2a::get_settings).post(routes::a2a::settings), ) .route( "/api/a2a/tokens", get(routes::a2a::list_tokens).post(routes::a2a::mint_token), ) .route("/api/a2a/tokens/{id}", delete(routes::a2a::revoke_token)) .route( "/api/a2a/{workspace}/.well-known/agents-card.json", get(routes::a2a::discovery_catalog), ) .route( "/api/a2a/{workspace}/{alias}/.well-known/agent-card.json", get(routes::a2a::discovery_card), ) .route("/api/a2a/{workspace}/{alias}", post(routes::a2a::task)) .route( "/api/claws/settings/full", get(routes::claws::settings_full), ) .route("/api/sessions", get(routes::sessions::list)) .route("/api/sessions", post(routes::sessions::create)) .route("/api/sessions/history", get(routes::sessions::history)) .route("/api/gateway", post(routes::gateway::gateway)) .route("/api/library/runs", post(routes::library::run)) .route("/api/library/items", get(routes::library::list)) .route("/api/routines", get(routes::routines::list)) .route("/api/routines", post(routes::routines::create)) .route("/api/routines/runs", get(routes::routines::runs)) .route("/api/skills", get(routes::skills::list)) .route("/api/skills/install", post(routes::skills::install)) .route("/api/skills/uninstall", post(routes::skills::uninstall)) .route("/api/openclaw/files", get(routes::files::openclaw_files)) .route( "/api/openclaw/files/content", get(routes::files::file_content), ) .route("/api/shared-drive/files", get(routes::files::shared_files)) .route("/api/slack/events", post(routes::slack::events)) .route( "/api/claws/{clawId}/browser/viewport.png", get(routes::browser::viewport), ) .route("/api/apps", get(routes::apps::directory)) .route("/api/apps/connect", post(routes::apps::connect)) .route("/api/apps/oauth/start", post(routes::oauth::start)) .route("/api/apps/oauth/callback", get(routes::oauth::callback)) .route("/api/apps/disconnect", post(routes::apps::disconnect)) .route("/api/approvals", get(routes::approvals::list)) .route("/api/approvals/{id}", get(routes::approvals::get)) .route( "/api/approvals/{id}/approve", post(routes::approvals::approve), ) .route( "/api/approvals/{id}/reject", post(routes::approvals::reject), ) .route("/api/team/claws", get(routes::team::claws)) .route("/api/team/members", get(routes::team::members)) .route("/api/team/orgchart", get(routes::team::orgchart)) .route("/api/team/leaderboard", get(routes::team::leaderboard)) .route("/api/team/credits", get(routes::team::credits)) .route("/api/team/usage", get(routes::billing::usage)) .route("/api/credits/redeem", post(routes::billing::redeem)) .route("/api/credits/checkout", post(routes::billing::checkout)) .route("/api/billing/config", get(routes::billing::billing_config)) .route("/api/billing/stripe", post(routes::billing::stripe_webhook)) .route("/api/team/permissions", get(routes::team::permissions)) .route("/api/topologies", get(routes::topology::catalog)) .route( "/api/topologies/classify", post(routes::topology::classify_graph), ) .route("/api/topologies/build", post(routes::topology::build_graph)) .route( "/api/topologies/compare", post(routes::topology::compare_topologies), ) .route("/api/topologies/run", post(routes::topology::run_topology)) .route("/api/swarm/run", post(routes::topology::run_swarm)) .route("/api/hooks/{token}", post(routes::webhooks::trigger_hook)) .route( "/api/teams/{id}/webhooks", get(routes::webhooks::list_webhooks).post(routes::webhooks::create_webhook), ) .route( "/api/teams", get(routes::teams::list_teams).post(routes::teams::create_team), ) .route( "/api/teams/from-claws", post(routes::teams::create_team_from_claws), ) .route( "/api/teams/{id}", get(routes::teams::get_team) .patch(routes::teams::patch_team) .delete(routes::teams::delete_team), ) .route( "/api/teams/{id}/name", axum::routing::patch(routes::teams::rename_team), ) .route("/api/teams/{id}/run", post(routes::teams::run_team)) .route( "/api/teams/{id}/runtime-config", axum::routing::patch(routes::teams::set_runtime_config), ) .route( "/api/teams/auto-provision", post(routes::teams::auto_provision), ) .route( "/api/companies", get(routes::companies::list_companies).post(routes::companies::create_company), ) .route( "/api/companies/{id}", get(routes::companies::get_company) .patch(routes::companies::patch_company) .delete(routes::companies::delete_company), ) .route( "/api/companies/{id}/name", axum::routing::patch(routes::companies::rename_company), ) .route( "/api/companies/{id}/run", post(routes::companies::run_company), ) .route( "/api/orgs", get(routes::orgs::list_orgs).post(routes::orgs::create_org), ) .route( "/api/orgs/{id}", get(routes::orgs::get_org).delete(routes::orgs::delete_org), ) .route( "/api/orgs/{id}/name", axum::routing::patch(routes::orgs::rename_org), ) .route("/api/orgs/{id}/run", post(routes::orgs::run_org)) // Team templates — Slice 3 read-only surface. Server upserts // builtins from TOML on boot; workspace-authored templates // land here in a later slice. .route("/api/team-templates", get(routes::team_templates::list)) .route("/api/team-templates/{id}", get(routes::team_templates::get)) // Skills catalog — Slice 3.5a. Layered by 3.5b's MCP server. // Mounted at /api/skills-catalog to avoid collision with the // legacy /api/skills surface until Slice 9 drops it. .route("/api/skills-catalog", get(routes::skills_catalog::list)) .route("/api/skills-catalog/{id}", get(routes::skills_catalog::get)) .route( "/api/claws/{id}/skill-bundle", get(routes::skills_catalog::effective_for_agent), ) // Missions — Slice 1 skeleton. Runs in parallel with the // legacy research/loops routes until Slice 9's cutover. .route( "/api/missions", get(routes::missions::list).post(routes::missions::create), ) // The roster grouped by mission — what "My Workforce" renders. .route("/api/workforce", get(routes::missions::workforce)) // The workflow recipe catalog (templates/workflows/*.toml). Serving it // lets the client stop mirroring the phase composition table inline. .route("/api/workflows", get(routes::missions::list_workflows)) // The private podcast feed. Token in the query string, not a header: // no podcast app can set headers. See `routes::podcast`. .route("/api/podcast/feed.xml", get(routes::podcast::feed)) .route("/api/podcast/episodes", get(routes::podcast::list_episodes)) .route("/api/podcast/subscription", get(routes::podcast::subscription)) .route( "/api/podcast/episodes/{file}", get(routes::podcast::episode_audio), ) .route( "/api/missions/{id}", get(routes::missions::get) .patch(routes::missions::update_meta) .delete(routes::missions::delete), ) .route( "/api/missions/{id}/status", axum::routing::patch(routes::missions::set_status), ) .route("/api/missions/{id}/refine", post(routes::missions::refine)) // Draft-less sibling: the wizard polishes a description before any // mission exists, so there is no id to route on. Declared BEFORE the // `{id}` routes would otherwise be ambiguous — axum matches literal // segments first, but keeping them adjacent makes the pair obvious. .route( "/api/missions/refine-draft", post(routes::missions::refine_draft), ) .route( "/api/missions/{id}/merge", post(routes::missions::merge_branch), ) .route( "/api/missions/{id}/artifacts/{artifact_id}/content", get(routes::missions::artifact_content), ) .route( "/api/missions/{id}/artifacts/{artifact_id}/download", get(routes::missions::artifact_download), ) // Slice 5: let a model size the mission's team. Proposing, listing and // deciding are separate verbs because only the last one spends money. // W1/#13: let a model author the phases, on the same propose → review → // approve shape as the roster above. .route( "/api/missions/{id}/plan-proposals", get(routes::mission_plan::list).post(routes::mission_plan::suggest), ) .route( "/api/missions/{id}/plan-proposals/{pid}/decide", post(routes::mission_plan::decide), ) .route( "/api/missions/{id}/team-proposals", get(routes::mission_roster::list).post(routes::mission_roster::suggest), ) .route( "/api/missions/{id}/team-proposals/{pid}/decide", post(routes::mission_roster::decide), ) .route( "/api/missions/{id}/herdr-dispatch", post(routes::missions::herdr_dispatch), ) .route( "/api/missions/{id}/description", patch(routes::missions::set_description), ) .route("/api/missions/{id}/runs", get(routes::missions::list_runs)) .route( "/api/missions/{id}/documents", get(routes::missions::list_documents), ) .route( "/api/missions/{id}/documents/{run_id}/{index}", get(routes::missions::get_document), ) .route( "/api/missions/{id}/phases/{phase_id}/retry", post(routes::missions::retry_phase), ) .route( "/api/missions/{id}/phases/{phase_id}/summary", get(routes::missions::get_phase_summary), ) .route( "/api/missions/{id}/phases/{phase_id}/evaluations", get(routes::missions::list_phase_evaluations), ) .route( "/api/missions/{id}/skill-use", get(routes::missions::skill_use), ) .route( "/api/missions/{id}/teams", get(routes::missions::list_teams), ) .route( "/api/missions/{id}/benchmark", post(routes::missions::trigger_benchmark), ) .route( "/api/missions/{id}/security-scan", post(routes::missions::trigger_security_scan), ) // Level-up (Slice 8.5) .route( "/api/level-up-proposals", get(routes::level_up::list_pending), ) .route( "/api/level-up-proposals/{id}", get(routes::level_up::get_proposal), ) .route( "/api/level-up-proposals/{id}/apply", post(routes::level_up::apply_proposal), ) .route( "/api/level-up-proposals/{id}/reject", post(routes::level_up::reject_proposal), ) .route( "/api/claws/{id}/level-up", post(routes::level_up::propose_for_agent), ) .route( "/api/teams/{id}/level-up", post(routes::level_up::propose_for_team), ) // (research + loops + wizard_repo routes retired in Slice 9 // cleanup — missions is the single workflow surface. Probe // is kept below if still referenced by any tool.) .route("/api/structure/stats", get(routes::structure::stats)) .route( "/api/structure/orphan-counts", get(routes::structure::orphan_counts), ) .route( "/api/structure/ensure-chain", post(routes::structure::ensure_chain), ) .route( "/api/structure/reify-orphans", post(routes::structure::reify_orphans), ) .route("/api/structure/{level}/{id}", get(routes::structure::node)) .route("/api/topology-runs", get(routes::topology::list_runs)) .route("/api/topology-runs/{id}", get(routes::topology::get_run)) .route( "/api/topology-runs/{id}/events", get(routes::topology::run_events_sse), ) .route( "/api/topology-runs/{id}/cancel", post(routes::topology::cancel_run), ) .route( "/api/topology-runs/{id}/output", get(routes::topology::get_run_output), ) // Repos tier — provider connections + cached repo list. .route( "/api/repos/connections", get(routes::repos::list_connections).post(routes::repos::create_connection), ) .route( "/api/repos/connections/{id}", get(routes::repos::get_connection) .patch(routes::repos::update_connection) .delete(routes::repos::delete_connection), ) .route( "/api/repos/connections/{id}/sync", post(routes::repos::sync_now), ) .route("/api/repos", get(routes::repos::list_repos)) .route("/api/repos/{id}", get(routes::repos::get_repo)) .layer(tower_http::trace::TraceLayer::new_for_http()) .with_state(state) }