Container-tier missions held the platform's provider keys in their environment, readable by an agent with Bash and public egress. With CLAWMATES_LLM_PROXY=1 (and CLAWMATES_LLM_PROXY_SECRET), a container instead holds a per-mission token where each key was, ANTHROPIC_BASE_URL points at the server's proxy on :8089 (not published, not routed by Traefik), and the GLM/Kimi hops' base URLs are rewritten in the mission's config copy. The proxy verifies the token (HMAC(secret, mission_id) — stateless, survives redeploys), refuses it unless the mission is running, swaps in the real credential and streams the response. Spiked first on gw-04: Claude Code on a subscription OAuth token, given only a placeholder and a base URL, sent nothing but POST /v1/messages there and answered once the placeholder was swapped. Off by default; no behaviour change until enabled. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
696 lines
25 KiB
Rust
696 lines
25 KiB
Rust
//! 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;
|
|
pub mod judge_quota;
|
|
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 delivery_secrets;
|
|
pub mod llm_proxy;
|
|
pub mod mission_delivery;
|
|
pub mod podcast;
|
|
pub mod mission_events;
|
|
pub mod mission_fs;
|
|
pub mod mission_memory;
|
|
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_triage;
|
|
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<std::path::PathBuf>,
|
|
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<std::path::PathBuf>,
|
|
/// Live control channels to connected fleet-node daemons.
|
|
pub node_hub: std::sync::Arc<fleet::NodeHub>,
|
|
/// The shelf. Present once the server wires storage; `None` in the
|
|
/// bare-`new` path used by tests that never touch blobs.
|
|
pub blobs: Option<std::sync::Arc<dyn cm_files::BlobStore>>,
|
|
}
|
|
|
|
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<std::path::PathBuf>) -> AppState {
|
|
self.file_root = root;
|
|
self
|
|
}
|
|
|
|
/// The shelf — where the paper library stores PDFs.
|
|
pub fn with_blobs(mut self, blobs: std::sync::Arc<dyn cm_files::BlobStore>) -> 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<std::sync::Arc<cm_auth::JwtVerifier>>,
|
|
) -> 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<cm_auth::JwtVerifier>,
|
|
) -> 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<fleet::NodeHub>) -> 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/judge/quota", get(routes::nodes::judge_quota))
|
|
.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)
|
|
}
|