Skip to content

Commit 686c44f

Browse files
committed
feat: generic bridge API for multi-challenge support
Replace hardcoded term-challenge routes with generic bridge: - GET /api/v1/bridge - List available challenges - ANY /api/v1/bridge/{challenge_name}/*path - Proxy to any challenge Examples: POST /api/v1/bridge/term-challenge/submit GET /api/v1/bridge/term-challenge/leaderboard Challenge discovery via CHALLENGE_{NAME}_URL env or challenge manager.
1 parent 85c30da commit 686c44f

File tree

2 files changed

+140
-92
lines changed

2 files changed

+140
-92
lines changed
Lines changed: 134 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,89 @@
1-
//! Bridge API - Proxy submissions to term-challenge
1+
//! Bridge API - Generic proxy to challenge containers
22
//!
3-
//! This module provides bridge endpoints that proxy requests to the
4-
//! term-challenge container for the centralized submission flow.
3+
//! This module provides a generic bridge endpoint that proxies requests to
4+
//! any challenge container via `/api/v1/bridge/{challenge_name}/*`
5+
//!
6+
//! Example:
7+
//! POST /api/v1/bridge/term-challenge/submit
8+
//! GET /api/v1/bridge/term-challenge/leaderboard
9+
//! POST /api/v1/bridge/math-challenge/submit
510
611
use crate::state::AppState;
712
use axum::{
813
body::Body,
9-
extract::State,
14+
extract::{Path, State},
1015
http::{Request, StatusCode},
1116
response::{IntoResponse, Response},
17+
Json,
1218
};
1319
use std::sync::Arc;
1420
use tracing::{debug, error, info, warn};
1521

16-
/// Default challenge ID for term-challenge
17-
const TERM_CHALLENGE_ID: &str = "term-challenge";
22+
/// Get challenge endpoint URL by name
23+
/// Looks up in challenge manager or environment variable CHALLENGE_{NAME}_URL
24+
fn get_challenge_url(state: &AppState, challenge_name: &str) -> Option<String> {
25+
// Normalize challenge name (replace - with _)
26+
let env_name = challenge_name.to_uppercase().replace('-', "_");
1827

19-
/// Get the term-challenge endpoint URL from environment or challenge manager
20-
fn get_term_challenge_url(state: &AppState) -> Option<String> {
21-
// First check environment variable
22-
if let Ok(url) = std::env::var("TERM_CHALLENGE_URL") {
28+
// First check environment variable: CHALLENGE_{NAME}_URL
29+
let env_key = format!("CHALLENGE_{}_URL", env_name);
30+
if let Ok(url) = std::env::var(&env_key) {
31+
debug!("Found {} = {}", env_key, url);
2332
return Some(url);
2433
}
2534

35+
// Also check legacy TERM_CHALLENGE_URL for backward compatibility
36+
if challenge_name == "term-challenge" || challenge_name == "term" {
37+
if let Ok(url) = std::env::var("TERM_CHALLENGE_URL") {
38+
return Some(url);
39+
}
40+
}
41+
2642
// Then check challenge manager
2743
if let Some(ref manager) = state.challenge_manager {
28-
// Try multiple possible IDs
29-
for id in &[
30-
TERM_CHALLENGE_ID,
31-
"term-challenge-server",
32-
"42b0dd5c-894f-3281-136a-ce34a0971d9f",
33-
] {
34-
if let Some(endpoint) = manager.get_endpoint(id) {
35-
return Some(endpoint);
36-
}
44+
// Try exact name
45+
if let Some(endpoint) = manager.get_endpoint(challenge_name) {
46+
return Some(endpoint);
47+
}
48+
49+
// Try with -server suffix
50+
let with_server = format!("{}-server", challenge_name);
51+
if let Some(endpoint) = manager.get_endpoint(&with_server) {
52+
return Some(endpoint);
53+
}
54+
55+
// Try challenge- prefix
56+
let with_prefix = format!("challenge-{}", challenge_name);
57+
if let Some(endpoint) = manager.get_endpoint(&with_prefix) {
58+
return Some(endpoint);
3759
}
3860
}
3961

4062
None
4163
}
4264

43-
/// Proxy a request to term-challenge
44-
async fn proxy_to_term_challenge(state: &AppState, path: &str, request: Request<Body>) -> Response {
45-
let base_url = match get_term_challenge_url(state) {
65+
/// Generic proxy to any challenge
66+
async fn proxy_to_challenge(
67+
state: &AppState,
68+
challenge_name: &str,
69+
path: &str,
70+
request: Request<Body>,
71+
) -> Response {
72+
let base_url = match get_challenge_url(state, challenge_name) {
4673
Some(url) => url,
4774
None => {
48-
warn!("Term-challenge not available - no endpoint configured");
75+
warn!(
76+
"Challenge '{}' not available - no endpoint configured",
77+
challenge_name
78+
);
4979
return (
50-
StatusCode::SERVICE_UNAVAILABLE,
51-
"Term-challenge service not available. Configure TERM_CHALLENGE_URL or ensure challenge is running.",
80+
StatusCode::NOT_FOUND,
81+
Json(serde_json::json!({
82+
"error": "Challenge not found",
83+
"challenge": challenge_name,
84+
"hint": format!("Set CHALLENGE_{}_URL or ensure challenge is running",
85+
challenge_name.to_uppercase().replace('-', "_"))
86+
})),
5287
)
5388
.into_response();
5489
}
@@ -59,7 +94,10 @@ async fn proxy_to_term_challenge(state: &AppState, path: &str, request: Request<
5994
base_url.trim_end_matches('/'),
6095
path.trim_start_matches('/')
6196
);
62-
debug!("Proxying to term-challenge: {}", url);
97+
debug!(
98+
"Proxying to challenge '{}': {} -> {}",
99+
challenge_name, path, url
100+
);
63101

64102
let method = request.method().clone();
65103
let headers = request.headers().clone();
@@ -73,7 +111,7 @@ async fn proxy_to_term_challenge(state: &AppState, path: &str, request: Request<
73111
};
74112

75113
let client = reqwest::Client::builder()
76-
.timeout(std::time::Duration::from_secs(60))
114+
.timeout(std::time::Duration::from_secs(120))
77115
.build()
78116
.unwrap();
79117

@@ -111,71 +149,93 @@ async fn proxy_to_term_challenge(state: &AppState, path: &str, request: Request<
111149
}
112150
Err(e) => {
113151
if e.is_timeout() {
114-
(StatusCode::GATEWAY_TIMEOUT, "Term-challenge timeout").into_response()
152+
(
153+
StatusCode::GATEWAY_TIMEOUT,
154+
Json(serde_json::json!({
155+
"error": "Challenge timeout",
156+
"challenge": challenge_name
157+
})),
158+
)
159+
.into_response()
115160
} else if e.is_connect() {
116-
warn!("Cannot connect to term-challenge at {}: {}", base_url, e);
161+
warn!(
162+
"Cannot connect to challenge '{}' at {}: {}",
163+
challenge_name, base_url, e
164+
);
117165
(
118166
StatusCode::SERVICE_UNAVAILABLE,
119-
"Term-challenge not reachable",
167+
Json(serde_json::json!({
168+
"error": "Challenge not reachable",
169+
"challenge": challenge_name,
170+
"url": base_url
171+
})),
120172
)
121173
.into_response()
122174
} else {
123-
error!("Proxy error: {}", e);
124-
(StatusCode::BAD_GATEWAY, format!("Proxy error: {}", e)).into_response()
175+
error!("Proxy error for '{}': {}", challenge_name, e);
176+
(
177+
StatusCode::BAD_GATEWAY,
178+
Json(serde_json::json!({
179+
"error": format!("Proxy error: {}", e),
180+
"challenge": challenge_name
181+
})),
182+
)
183+
.into_response()
125184
}
126185
}
127186
}
128187
}
129188

130-
/// POST /api/v1/submit - Bridge to term-challenge submit endpoint
131-
pub async fn bridge_submit(State(state): State<Arc<AppState>>, request: Request<Body>) -> Response {
132-
info!("Bridging submit request to term-challenge");
133-
proxy_to_term_challenge(&state, "/api/v1/submit", request).await
134-
}
135-
136-
/// GET /api/v1/challenge/leaderboard - Bridge to term-challenge leaderboard
137-
pub async fn bridge_leaderboard(
189+
/// ANY /api/v1/bridge/{challenge_name}/*path - Generic bridge to any challenge
190+
///
191+
/// Routes:
192+
/// /api/v1/bridge/term-challenge/submit -> term-challenge /api/v1/submit
193+
/// /api/v1/bridge/term-challenge/leaderboard -> term-challenge /api/v1/leaderboard
194+
/// /api/v1/bridge/math-challenge/evaluate -> math-challenge /api/v1/evaluate
195+
pub async fn bridge_to_challenge(
138196
State(state): State<Arc<AppState>>,
197+
Path((challenge_name, path)): Path<(String, String)>,
139198
request: Request<Body>,
140199
) -> Response {
141-
proxy_to_term_challenge(&state, "/api/v1/leaderboard", request).await
142-
}
200+
info!(
201+
"Bridge request: challenge='{}' path='/{}'",
202+
challenge_name, path
203+
);
143204

144-
/// GET /api/v1/challenge/status - Bridge to term-challenge status
145-
pub async fn bridge_status(State(state): State<Arc<AppState>>, request: Request<Body>) -> Response {
146-
proxy_to_term_challenge(&state, "/api/v1/status", request).await
147-
}
205+
// Construct the API path (add /api/v1/ prefix if not present)
206+
let api_path = if path.starts_with("api/") {
207+
format!("/{}", path)
208+
} else {
209+
format!("/api/v1/{}", path)
210+
};
148211

149-
/// POST /api/v1/my/agents - Bridge to term-challenge my agents
150-
pub async fn bridge_my_agents(
151-
State(state): State<Arc<AppState>>,
152-
request: Request<Body>,
153-
) -> Response {
154-
proxy_to_term_challenge(&state, "/api/v1/my/agents", request).await
212+
proxy_to_challenge(&state, &challenge_name, &api_path, request).await
155213
}
156214

157-
/// POST /api/v1/my/agents/:hash/source - Bridge to term-challenge source
158-
pub async fn bridge_my_agent_source(
159-
State(state): State<Arc<AppState>>,
160-
axum::extract::Path(hash): axum::extract::Path<String>,
161-
request: Request<Body>,
162-
) -> Response {
163-
let path = format!("/api/v1/my/agents/{}/source", hash);
164-
proxy_to_term_challenge(&state, &path, request).await
165-
}
215+
/// GET /api/v1/bridge - List available challenges
216+
pub async fn list_bridges(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
217+
let mut challenges = vec![];
166218

167-
/// POST /api/v1/validator/claim_job - Bridge to term-challenge validator claim
168-
pub async fn bridge_validator_claim(
169-
State(state): State<Arc<AppState>>,
170-
request: Request<Body>,
171-
) -> Response {
172-
proxy_to_term_challenge(&state, "/api/v1/validator/claim_job", request).await
173-
}
219+
// Check known challenge environment variables
220+
for name in &["TERM_CHALLENGE", "MATH_CHALLENGE", "CODE_CHALLENGE"] {
221+
let env_key = format!("{}_URL", name);
222+
if std::env::var(&env_key).is_ok() {
223+
challenges.push(name.to_lowercase().replace('_', "-"));
224+
}
225+
}
174226

175-
/// POST /api/v1/validator/complete_job - Bridge to term-challenge validator complete
176-
pub async fn bridge_validator_complete(
177-
State(state): State<Arc<AppState>>,
178-
request: Request<Body>,
179-
) -> Response {
180-
proxy_to_term_challenge(&state, "/api/v1/validator/complete_job", request).await
227+
// Add from challenge manager
228+
if let Some(ref manager) = state.challenge_manager {
229+
for id in manager.list_challenge_ids() {
230+
if !challenges.contains(&id) {
231+
challenges.push(id);
232+
}
233+
}
234+
}
235+
236+
Json(serde_json::json!({
237+
"bridges": challenges,
238+
"usage": "/api/v1/bridge/{challenge_name}/{path}",
239+
"example": "/api/v1/bridge/term-challenge/submit"
240+
}))
181241
}

crates/platform-server/src/main.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -182,25 +182,13 @@ async fn main() -> anyhow::Result<()> {
182182
get(data_api::get_results),
183183
)
184184
.route("/api/v1/data/snapshot", get(data_api::get_snapshot))
185-
// === BRIDGE TO TERM-CHALLENGE (new submission flow) ===
186-
.route("/api/v1/submit", post(api::bridge::bridge_submit))
185+
// === BRIDGE TO CHALLENGES (generic proxy) ===
186+
// Usage: /api/v1/bridge/{challenge_name}/{path}
187+
// Example: /api/v1/bridge/term-challenge/submit
188+
.route("/api/v1/bridge", get(api::bridge::list_bridges))
187189
.route(
188-
"/api/v1/challenge/leaderboard",
189-
get(api::bridge::bridge_leaderboard),
190-
)
191-
.route("/api/v1/challenge/status", get(api::bridge::bridge_status))
192-
.route("/api/v1/my/agents", post(api::bridge::bridge_my_agents))
193-
.route(
194-
"/api/v1/my/agents/:hash/source",
195-
post(api::bridge::bridge_my_agent_source),
196-
)
197-
.route(
198-
"/api/v1/validator/claim_job",
199-
post(api::bridge::bridge_validator_claim),
200-
)
201-
.route(
202-
"/api/v1/validator/complete_job",
203-
post(api::bridge::bridge_validator_complete),
190+
"/api/v1/bridge/:challenge/*path",
191+
any(api::bridge::bridge_to_challenge),
204192
)
205193
// === SUBMISSIONS & EVALUATIONS (deprecated - use /api/v1/submit) ===
206194
.route(

0 commit comments

Comments
 (0)