Three gaps between "the pipeline works" and "you can use it".
**1. Topics could not be set.** The wizard never sent `config.topics`, so every
mission created through the UI silently fell back to
`library::default_topics()` — a hardcoded list that is somebody else's research
interests. The card now takes one arXiv search per line, and the description
field says plainly that for this template it IS the brief the agents judge
relevance against.
**2. There was nowhere to see or subscribe.** New PODCAST tier in the left rail,
between AGENT and REPOS: the feed URL with a copy button, the episode list, and
an inline player for checking one at a desk. `GET /api/podcast/episodes` and
`/subscription` back it. The panel also reports how many missions produced no
audio, so a missing day reads as a known gap rather than silence.
**3. The feed 404'd for the only client that will ever request it.** Three
layers each assumed a browser:
- `resolveBearer` is server-only (`next/headers`), so a client component that
imported it broke the build outright. The panel now goes through the
same-origin proxy like every other panel, and the backend mints the feed URL
because the session lives in an httpOnly cookie JavaScript cannot read.
- The `/api` proxy demanded a session COOKIE. A podcast app has none and
carries `?token=` instead — the same shape as the existing `hooks/` prefix,
which is already exempt for exactly this reason.
- The local autologin middleware 307'd it to `/auth/autologin`. A podcast app
follows redirects blindly and would have stored an HTML page as the episode.
Neither exemption weakens auth: the backend still validates the token and
answers 401 to a bad one, verified. `episode_audio` accepts the token from
either the query string or an Authorization header, because the app fetches it
one way and the browser player the other, and refusing either breaks one of the
two ways this is listened to.
`CLAWMATES_PUBLIC_URL` matters and was wrong first: the tailnet root proxies to
a different service on :18789, and this frontend is on :8443. A feed advertising
an unreachable origin syncs silently forever, so `/subscription` returns a
`reachable` flag and the panel warns when it is still localhost.
Verified from a phone's point of view: feed 200 application/rss+xml over the
tailnet, enclosure 200 with 6,739,582 bytes of audio at 421s, bad token 401.
367 tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
680 lines
25 KiB
Rust
680 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;
|
|
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;
|
|
mod runtime_provision;
|
|
pub mod security_scan;
|
|
pub mod session_executor;
|
|
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;
|
|
mod topology_exec;
|
|
pub mod topology_worker;
|
|
pub mod validator_preflight;
|
|
pub mod vm_placement;
|
|
pub mod vm_stop_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/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}/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)
|
|
}
|