Skip to content

Commit 85c30da

Browse files
committed
feat: add bridge API to proxy submissions to term-challenge
New endpoints that proxy to term-challenge: - POST /api/v1/submit - Bridge to term-challenge submit - GET /api/v1/challenge/leaderboard - Bridge to leaderboard - GET /api/v1/challenge/status - Bridge to status - POST /api/v1/my/agents - Bridge to list own agents - POST /api/v1/my/agents/:hash/source - Bridge to get own source - POST /api/v1/validator/claim_job - Bridge to validator claim - POST /api/v1/validator/complete_job - Bridge to validator complete The bridge uses TERM_CHALLENGE_URL env var or discovers the endpoint via challenge manager.
1 parent d187022 commit 85c30da

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-1
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//! Bridge API - Proxy submissions to term-challenge
2+
//!
3+
//! This module provides bridge endpoints that proxy requests to the
4+
//! term-challenge container for the centralized submission flow.
5+
6+
use crate::state::AppState;
7+
use axum::{
8+
body::Body,
9+
extract::State,
10+
http::{Request, StatusCode},
11+
response::{IntoResponse, Response},
12+
};
13+
use std::sync::Arc;
14+
use tracing::{debug, error, info, warn};
15+
16+
/// Default challenge ID for term-challenge
17+
const TERM_CHALLENGE_ID: &str = "term-challenge";
18+
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") {
23+
return Some(url);
24+
}
25+
26+
// Then check challenge manager
27+
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+
}
37+
}
38+
}
39+
40+
None
41+
}
42+
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) {
46+
Some(url) => url,
47+
None => {
48+
warn!("Term-challenge not available - no endpoint configured");
49+
return (
50+
StatusCode::SERVICE_UNAVAILABLE,
51+
"Term-challenge service not available. Configure TERM_CHALLENGE_URL or ensure challenge is running.",
52+
)
53+
.into_response();
54+
}
55+
};
56+
57+
let url = format!(
58+
"{}/{}",
59+
base_url.trim_end_matches('/'),
60+
path.trim_start_matches('/')
61+
);
62+
debug!("Proxying to term-challenge: {}", url);
63+
64+
let method = request.method().clone();
65+
let headers = request.headers().clone();
66+
67+
let body_bytes = match axum::body::to_bytes(request.into_body(), 10 * 1024 * 1024).await {
68+
Ok(bytes) => bytes,
69+
Err(e) => {
70+
error!("Failed to read request body: {}", e);
71+
return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response();
72+
}
73+
};
74+
75+
let client = reqwest::Client::builder()
76+
.timeout(std::time::Duration::from_secs(60))
77+
.build()
78+
.unwrap();
79+
80+
let mut req_builder = client.request(method, &url);
81+
for (key, value) in headers.iter() {
82+
if key != "host" && key != "content-length" {
83+
req_builder = req_builder.header(key, value);
84+
}
85+
}
86+
87+
if !body_bytes.is_empty() {
88+
req_builder = req_builder.body(body_bytes.to_vec());
89+
}
90+
91+
match req_builder.send().await {
92+
Ok(resp) => {
93+
let status = resp.status();
94+
let headers = resp.headers().clone();
95+
96+
match resp.bytes().await {
97+
Ok(body) => {
98+
let mut response = Response::builder().status(status);
99+
for (key, value) in headers.iter() {
100+
response = response.header(key, value);
101+
}
102+
response
103+
.body(Body::from(body))
104+
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
105+
}
106+
Err(e) => {
107+
error!("Failed to read response: {}", e);
108+
StatusCode::BAD_GATEWAY.into_response()
109+
}
110+
}
111+
}
112+
Err(e) => {
113+
if e.is_timeout() {
114+
(StatusCode::GATEWAY_TIMEOUT, "Term-challenge timeout").into_response()
115+
} else if e.is_connect() {
116+
warn!("Cannot connect to term-challenge at {}: {}", base_url, e);
117+
(
118+
StatusCode::SERVICE_UNAVAILABLE,
119+
"Term-challenge not reachable",
120+
)
121+
.into_response()
122+
} else {
123+
error!("Proxy error: {}", e);
124+
(StatusCode::BAD_GATEWAY, format!("Proxy error: {}", e)).into_response()
125+
}
126+
}
127+
}
128+
}
129+
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(
138+
State(state): State<Arc<AppState>>,
139+
request: Request<Body>,
140+
) -> Response {
141+
proxy_to_term_challenge(&state, "/api/v1/leaderboard", request).await
142+
}
143+
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+
}
148+
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
155+
}
156+
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+
}
166+
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+
}
174+
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
181+
}

crates/platform-server/src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! API handlers
22
33
pub mod auth;
4+
pub mod bridge;
45
pub mod challenges;
56
pub mod evaluations;
67
pub mod jobs;

crates/platform-server/src/main.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,27 @@ async fn main() -> anyhow::Result<()> {
182182
get(data_api::get_results),
183183
)
184184
.route("/api/v1/data/snapshot", get(data_api::get_snapshot))
185-
// === SUBMISSIONS & EVALUATIONS ===
185+
// === BRIDGE TO TERM-CHALLENGE (new submission flow) ===
186+
.route("/api/v1/submit", post(api::bridge::bridge_submit))
187+
.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),
204+
)
205+
// === SUBMISSIONS & EVALUATIONS (deprecated - use /api/v1/submit) ===
186206
.route(
187207
"/api/v1/submissions",
188208
get(api::submissions::list_submissions),

0 commit comments

Comments
 (0)