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
611use crate :: state:: AppState ;
712use axum:: {
813 body:: Body ,
9- extract:: State ,
14+ extract:: { Path , State } ,
1015 http:: { Request , StatusCode } ,
1116 response:: { IntoResponse , Response } ,
17+ Json ,
1218} ;
1319use std:: sync:: Arc ;
1420use 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}
0 commit comments