Trigger — did the agent reach for the skill when it applied? — cannot be measured while every body is inlined into the prompt. Nothing was reached for. `skill_use` has been reporting `NotObservable` for that reason, and it was right to. The skills door made retrieval possible; this makes it a delivery arm. `index` sends each pinned skill's name, description, `when_to_use` and the uri that returns its body, and the agent fetches what it judges relevant. `inline` is unchanged and stays the default. An A/B rather than a switch, because `index` can only cost Compliance: under `inline` the procedure sits in front of the model whether or not it noticed it applied. Trading a measured axis for an unmeasured regression in another is not an improvement, so both arms stay runnable and the arm is recorded on the mission row. Three things the mechanism refuses to do: - `index` without a door falls back to `inline`. An index names bodies and says how to fetch them; with no `clawmates_skills` server reachable that is a list of dead ends, and it fails as an agent ignoring its skills rather than as a missing config. `install_skills_door` now returns whether it installed, because the caller needs the answer and not just the log line. - The scorer reads the arm off the recorded PROMPT, not off the mission row. The row says what the mission is configured to do now; the score is being computed against a turn that ran then. - Under `index`, a skill that was offered and never read is a Fail, not the inline arm's `NotObservable` — but only where the skill had a checkable consequence in that phase. Reusing the inline text would have said "this skill was inlined into the prompt" about a skill whose body was never sent, and scoring a real miss as a structural blind spot is the failure this measurement already made once. The arm is per mission (`config.skill_delivery`), not only per deployment. Both arms run against one server process; restarting between them would put a confound in the comparison that the numbers would not show. 829 tests, 108 binaries, green. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
690 lines
25 KiB
Rust
690 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;
|
|
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<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}/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)
|
|
}
|