Skip to main content

agent_runtime_http_server/
lib.rs

1//! Axum transport for the Runtime Open API.
2
3use std::{convert::Infallible, sync::Arc, time::Duration};
4
5use async_trait::async_trait;
6use axum::{
7    Json, Router,
8    extract::{
9        DefaultBodyLimit, Path, Query, Request, State,
10        rejection::JsonRejection,
11        ws::{Message, WebSocket, WebSocketUpgrade},
12    },
13    http::{HeaderMap, StatusCode, header},
14    middleware::{self, Next},
15    response::{
16        IntoResponse, Response, Sse,
17        sse::{Event, KeepAlive},
18    },
19    routing::{get, post},
20};
21use futures_util::{SinkExt, Stream, StreamExt};
22use runtime_api_contract::{
23    AgentTask, AgentTaskEvent, AgentTaskEventPage, AgentTaskWebSocketClientMessage,
24    AgentTaskWebSocketServerMessage, ApiErrorBody, ClaudeContentBlock, ClaudeError,
25    ClaudeErrorEnvelope, ClaudeMessage, ClaudeMessageRequest, ClaudeUsage, CreateAgentTaskRequest,
26    CreateExecutionRequest, ExecutionOptions, ExecutionState, ModelGenerationOptions,
27    ModelToolChoice, OpenAiError, OpenAiErrorEnvelope, OpenAiInputTokensDetails,
28    OpenAiOutputContent, OpenAiOutputItem, OpenAiOutputTokensDetails, OpenAiResponse,
29    OpenAiResponseError, OpenAiResponseRequest, OpenAiUsage, RuntimeInput, RuntimeMessage,
30    RuntimeMessageRole, SubmitInputRequest, openapi_document,
31};
32use runtime_link_api::{LinkError, RuntimeLink};
33use runtime_types::{
34    CallerScope, CredentialHandle, ExecutionId, RequestAuthority, RuntimeInstanceId,
35};
36use serde::Deserialize;
37use serde_json::json;
38use sha2::{Digest, Sha256};
39
40#[derive(Debug)]
41pub struct AuthError {
42    pub message: String,
43}
44
45#[async_trait]
46pub trait IngressAuthenticator: Send + Sync + 'static {
47    async fn authenticate(&self, headers: &HeaderMap) -> Result<RequestAuthority, AuthError>;
48}
49
50/// Safe single-tenant authenticator for local/container deployments.
51/// Production multi-tenant hosts should inject a JWT/introspection adapter.
52pub struct StaticTokenAuthenticator {
53    expected_token: String,
54    caller: CallerScope,
55}
56
57impl StaticTokenAuthenticator {
58    pub fn new(expected_token: impl Into<String>, caller: CallerScope) -> Result<Self, AuthError> {
59        let expected_token = expected_token.into();
60        if expected_token.is_empty() {
61            return Err(AuthError {
62                message: "static bearer token must not be empty".into(),
63            });
64        }
65        caller.validate().map_err(|error| AuthError {
66            message: error.to_string(),
67        })?;
68        Ok(Self {
69            expected_token,
70            caller,
71        })
72    }
73}
74
75#[async_trait]
76impl IngressAuthenticator for StaticTokenAuthenticator {
77    async fn authenticate(&self, headers: &HeaderMap) -> Result<RequestAuthority, AuthError> {
78        let bearer = headers
79            .get(header::AUTHORIZATION)
80            .and_then(|value| value.to_str().ok())
81            .and_then(|value| value.strip_prefix("Bearer "));
82        let api_key = headers
83            .get("x-api-key")
84            .and_then(|value| value.to_str().ok());
85        if bearer.is_some() && api_key.is_some() && bearer != api_key {
86            return Err(AuthError {
87                message: "Authorization and x-api-key credentials disagree".into(),
88            });
89        }
90        let value = bearer
91            .or(api_key)
92            .filter(|value| !value.is_empty())
93            .ok_or_else(|| AuthError {
94                message: "missing Authorization bearer or x-api-key credential".into(),
95            })?;
96        if value != self.expected_token {
97            return Err(AuthError {
98                message: "invalid token credential".into(),
99            });
100        }
101        Ok(RequestAuthority {
102            caller: self.caller.clone(),
103            credential: CredentialHandle::new(value).map_err(|error| AuthError {
104                message: error.to_string(),
105            })?,
106        })
107    }
108}
109
110/// Authenticator for a Runtime listener reachable only through a trusted
111/// identity-aware gateway. The gateway owns credential verification and
112/// injects the caller scope headers; Runtime forwards the bearer through an
113/// opaque credential handle to its scoped Agent Infra client.
114pub struct TrustedGatewayAuthenticator {
115    shared_secret_digest: [u8; 32],
116}
117
118impl TrustedGatewayAuthenticator {
119    pub fn new(shared_secret: &str) -> Result<Self, AuthError> {
120        if shared_secret.len() < 32 || shared_secret.len() > 4096 {
121            return Err(AuthError {
122                message: "trusted gateway secret must contain 32..=4096 bytes".into(),
123            });
124        }
125        Ok(Self {
126            shared_secret_digest: Sha256::digest(shared_secret.as_bytes()).into(),
127        })
128    }
129}
130
131#[async_trait]
132impl IngressAuthenticator for TrustedGatewayAuthenticator {
133    async fn authenticate(&self, headers: &HeaderMap) -> Result<RequestAuthority, AuthError> {
134        fn required(headers: &HeaderMap, name: &'static str) -> Result<String, AuthError> {
135            headers
136                .get(name)
137                .and_then(|value| value.to_str().ok())
138                .filter(|value| !value.is_empty())
139                .map(ToOwned::to_owned)
140                .ok_or_else(|| AuthError {
141                    message: format!("trusted gateway header {name} is missing"),
142                })
143        }
144        let supplied_secret = required(headers, "x-runtime-gateway-secret")?;
145        let supplied_digest: [u8; 32] = Sha256::digest(supplied_secret.as_bytes()).into();
146        let matches = self
147            .shared_secret_digest
148            .iter()
149            .zip(supplied_digest)
150            .fold(0_u8, |difference, (expected, actual)| {
151                difference | (*expected ^ actual)
152            })
153            == 0;
154        if !matches {
155            return Err(AuthError {
156                message: "trusted gateway authentication failed".into(),
157            });
158        }
159        let token = headers
160            .get(header::AUTHORIZATION)
161            .and_then(|value| value.to_str().ok())
162            .and_then(|value| value.strip_prefix("Bearer "))
163            .ok_or_else(|| AuthError {
164                message: "trusted gateway bearer credential is missing".into(),
165            })?;
166        let caller = CallerScope {
167            subject: required(headers, "x-runtime-subject")?,
168            tenant_id: required(headers, "x-runtime-tenant-id")?,
169            project_id: required(headers, "x-runtime-project-id")?,
170            capabilities: headers
171                .get("x-runtime-capabilities")
172                .and_then(|value| value.to_str().ok())
173                .unwrap_or_default()
174                .split(',')
175                .map(str::trim)
176                .filter(|value| !value.is_empty())
177                .map(ToOwned::to_owned)
178                .collect(),
179        };
180        caller.validate().map_err(|error| AuthError {
181            message: error.to_string(),
182        })?;
183        Ok(RequestAuthority {
184            caller,
185            credential: CredentialHandle::new(token).map_err(|error| AuthError {
186                message: error.to_string(),
187            })?,
188        })
189    }
190}
191
192#[async_trait]
193pub trait ReadinessProbe: Send + Sync + 'static {
194    async fn ready(&self) -> Result<(), String>;
195}
196
197pub struct AlwaysReady;
198#[async_trait]
199impl ReadinessProbe for AlwaysReady {
200    async fn ready(&self) -> Result<(), String> {
201        Ok(())
202    }
203}
204
205/// Explicit route selection for the Runtime's single public listener.
206///
207/// WebSocket is an HTTP upgrade route on this listener, not an independently
208/// spawned socket server. Health and OpenAPI remain registered even when an
209/// execution transport is disabled so operators can inspect the process.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct RuntimeTransportConfig {
212    pub agent_tasks: bool,
213    pub openai_responses: bool,
214    pub claude_messages: bool,
215    pub sse: bool,
216    pub websocket: bool,
217    pub websocket_max_message_bytes: usize,
218}
219
220#[derive(Clone)]
221struct AppState {
222    link: Arc<dyn RuntimeLink>,
223    authenticator: Arc<dyn IngressAuthenticator>,
224    readiness: Arc<dyn ReadinessProbe>,
225    transport: RuntimeTransportConfig,
226}
227
228/// Construct the complete public Runtime transport surface.
229///
230/// REST commands, replayable SSE, and execution-scoped WebSocket upgrades are
231/// registered independently. Both streaming protocols resume from a committed
232/// Journal sequence; WebSocket client commands are restricted to submit-input
233/// and cancel for the execution encoded in the route. Agent definitions cannot
234/// add routes or listen on ports.
235pub fn router(
236    link: Arc<dyn RuntimeLink>,
237    authenticator: Arc<dyn IngressAuthenticator>,
238    readiness: Arc<dyn ReadinessProbe>,
239    transport: RuntimeTransportConfig,
240) -> Router {
241    let state = AppState {
242        link,
243        authenticator,
244        readiness,
245        transport,
246    };
247    let mut router = Router::new()
248        .route("/livez", get(live))
249        .route("/readyz", get(ready))
250        .route("/openapi.json", get(openapi));
251    if transport.agent_tasks {
252        router = router
253            // Runtime-native Task API. This is the preferred surface for
254            // background work, task interaction, and observability.
255            .route("/v1/agent/tasks", post(create_agent_task))
256            .route("/v1/agent/tasks/{task_id}", get(get_agent_task))
257            .route(
258                "/v1/agent/tasks/{task_id}/events",
259                get(get_agent_task_events),
260            )
261            .route(
262                "/v1/agent/tasks/{task_id}/inputs",
263                post(submit_agent_task_input),
264            )
265            .route("/v1/agent/tasks/{task_id}/cancel", post(cancel_agent_task));
266    }
267    if transport.openai_responses {
268        router = router
269            .route("/v1/responses", post(create_openai_response))
270            .route("/v1/responses/{response_id}", get(get_openai_response))
271            .route(
272                "/v1/responses/{response_id}/cancel",
273                post(cancel_openai_response),
274            );
275    }
276    if transport.claude_messages {
277        router = router.route("/v1/messages", post(create_claude_message));
278    }
279    if transport.agent_tasks && transport.sse {
280        router = router.route(
281            "/v1/agent/tasks/{task_id}/stream",
282            get(stream_agent_task_events),
283        );
284    }
285    if transport.agent_tasks && transport.websocket {
286        let max_message_bytes = transport.websocket_max_message_bytes;
287        router = router.route(
288            "/v1/agent/tasks/{task_id}/ws",
289            get(
290                move |upgrade: WebSocketUpgrade,
291                      state: State<AppState>,
292                      headers: HeaderMap,
293                      path: Path<String>,
294                      query: Query<EventQuery>| {
295                    websocket_agent_task(
296                        upgrade.max_message_size(max_message_bytes),
297                        state,
298                        headers,
299                        path,
300                        query,
301                    )
302                },
303            ),
304        );
305    }
306    router
307        .layer(DefaultBodyLimit::max(1024 * 1024))
308        .layer(middleware::from_fn(request_id_middleware))
309        .with_state(state)
310}
311
312/// Add one correlation ID to every REST, SSE, WebSocket handshake, and error
313/// response. Trusted short IDs are preserved; malformed IDs are replaced.
314async fn request_id_middleware(mut request: Request, next: Next) -> Response {
315    let request_id = request
316        .headers()
317        .get("x-request-id")
318        .and_then(|value| value.to_str().ok())
319        .filter(|value| {
320            !value.is_empty() && value.len() <= 128 && !value.chars().any(char::is_control)
321        })
322        .map(ToOwned::to_owned)
323        .unwrap_or_else(|| format!("req_{}", uuid::Uuid::new_v4()));
324    if let Ok(value) = request_id.parse() {
325        request.headers_mut().insert("x-request-id", value);
326    }
327    let mut response = next.run(request).await;
328    if let Ok(value) = request_id.parse() {
329        response.headers_mut().insert("x-request-id", value);
330    }
331    if let Ok(value) = request_id.parse() {
332        response.headers_mut().insert("request-id", value);
333    }
334    response
335}
336
337async fn live() -> Json<serde_json::Value> {
338    Json(json!({"status":"ok","service":"agent-runtime"}))
339}
340
341async fn openapi(State(state): State<AppState>) -> Json<serde_json::Value> {
342    let mut document = openapi_document();
343    let paths = document["paths"]
344        .as_object_mut()
345        .expect("embedded OpenAPI paths must be an object");
346    if !state.transport.agent_tasks {
347        for path in [
348            "/v1/agent/tasks",
349            "/v1/agent/tasks/{taskId}",
350            "/v1/agent/tasks/{taskId}/events",
351            "/v1/agent/tasks/{taskId}/inputs",
352            "/v1/agent/tasks/{taskId}/cancel",
353            "/v1/agent/tasks/{taskId}/stream",
354            "/v1/agent/tasks/{taskId}/ws",
355        ] {
356            paths.remove(path);
357        }
358    }
359    if !state.transport.openai_responses {
360        for path in [
361            "/v1/responses",
362            "/v1/responses/{responseId}",
363            "/v1/responses/{responseId}/cancel",
364        ] {
365            paths.remove(path);
366        }
367    }
368    if !state.transport.claude_messages {
369        paths.remove("/v1/messages");
370    }
371    if !state.transport.sse {
372        paths.remove("/v1/agent/tasks/{taskId}/stream");
373    }
374    if !state.transport.websocket {
375        paths.remove("/v1/agent/tasks/{taskId}/ws");
376    }
377    Json(document)
378}
379
380async fn ready(State(state): State<AppState>) -> Response {
381    match state.readiness.ready().await {
382        Ok(()) => Json(json!({"status":"ready"})).into_response(),
383        Err(message) => (
384            StatusCode::SERVICE_UNAVAILABLE,
385            Json(json!({"status":"not_ready","message":message})),
386        )
387            .into_response(),
388    }
389}
390
391async fn create_agent_task(
392    State(state): State<AppState>,
393    headers: HeaderMap,
394    payload: Result<Json<CreateAgentTaskRequest>, JsonRejection>,
395) -> Response {
396    let authority = match authenticate(&state, &headers).await {
397        Ok(value) => value,
398        Err(response) => return response,
399    };
400    let Json(mut request) = match payload {
401        Ok(value) => value,
402        Err(error) => {
403            return api_error(
404                StatusCode::BAD_REQUEST,
405                "TASK_REQUEST_INVALID",
406                &error.body_text(),
407            );
408        }
409    };
410    attach_request_id_metadata(&headers, &mut request.metadata);
411    let key = idempotency_key_or_random(&headers, "task");
412    match state
413        .link
414        .create_execution(&authority, &key, request.into_execution())
415        .await
416    {
417        Ok(value) => {
418            let status = if value.replayed {
419                StatusCode::OK
420            } else {
421                StatusCode::ACCEPTED
422            };
423            let task = AgentTask::from(value.execution);
424            let mut response = (status, Json(task.clone())).into_response();
425            if let Ok(location) = task.links.self_url.parse() {
426                response.headers_mut().insert(header::LOCATION, location);
427            }
428            response
429                .headers_mut()
430                .insert(header::RETRY_AFTER, "1".parse().expect("static header"));
431            response
432        }
433        Err(error) => link_error(error),
434    }
435}
436
437async fn get_agent_task(
438    State(state): State<AppState>,
439    headers: HeaderMap,
440    Path(id): Path<String>,
441) -> Response {
442    let authority = match authenticate(&state, &headers).await {
443        Ok(value) => value,
444        Err(response) => return response,
445    };
446    let id = match parse_id(id) {
447        Ok(value) => value,
448        Err(error) => return invalid_task_id(error),
449    };
450    match state.link.execution(&authority, &id).await {
451        Ok(value) => Json(AgentTask::from(value)).into_response(),
452        Err(error) => link_error(error),
453    }
454}
455
456async fn get_agent_task_events(
457    State(state): State<AppState>,
458    headers: HeaderMap,
459    Path(id): Path<String>,
460    Query(query): Query<EventQuery>,
461) -> Response {
462    let authority = match authenticate(&state, &headers).await {
463        Ok(value) => value,
464        Err(response) => return response,
465    };
466    let id = match parse_id(id) {
467        Ok(value) => value,
468        Err(error) => return invalid_task_id(error),
469    };
470    let limit = query.limit.unwrap_or(100);
471    if !(1..=1000).contains(&limit) {
472        return api_error(
473            StatusCode::BAD_REQUEST,
474            "TASK_EVENT_LIMIT_INVALID",
475            "limit must be within 1..=1000",
476        );
477    }
478    match state.link.events(&authority, &id, query.after, limit).await {
479        Ok(page) => Json(AgentTaskEventPage {
480            items: page.items.into_iter().map(AgentTaskEvent::from).collect(),
481            next_after: page.next_after,
482        })
483        .into_response(),
484        Err(error) => link_error(error),
485    }
486}
487
488async fn stream_agent_task_events(
489    State(state): State<AppState>,
490    headers: HeaderMap,
491    Path(id): Path<String>,
492    Query(query): Query<EventQuery>,
493) -> Response {
494    let authority = match authenticate(&state, &headers).await {
495        Ok(value) => value,
496        Err(response) => return response,
497    };
498    let id = match parse_id(id) {
499        Ok(value) => value,
500        Err(error) => return invalid_task_id(error),
501    };
502    let after = match sse_resume_after(&headers, query.after) {
503        Ok(value) => value,
504        Err(message) => {
505            return api_error(StatusCode::BAD_REQUEST, "SSE_CURSOR_INVALID", &message);
506        }
507    };
508    if let Err(error) = state.link.execution(&authority, &id).await {
509        return link_error(error);
510    }
511    let stream = agent_task_event_stream(state.link, authority, id, after);
512    Sse::new(stream)
513        .keep_alive(
514            KeepAlive::new()
515                .interval(Duration::from_secs(15))
516                .text("heartbeat"),
517        )
518        .into_response()
519}
520
521async fn submit_agent_task_input(
522    State(state): State<AppState>,
523    headers: HeaderMap,
524    Path(id): Path<String>,
525    Json(request): Json<SubmitInputRequest>,
526) -> Response {
527    let authority = match authenticate(&state, &headers).await {
528        Ok(value) => value,
529        Err(response) => return response,
530    };
531    let id = match parse_id(id) {
532        Ok(value) => value,
533        Err(error) => return invalid_task_id(error),
534    };
535    match state.link.submit_input(&authority, &id, request).await {
536        Ok(value) => Json(AgentTask::from(value)).into_response(),
537        Err(error) => link_error(error),
538    }
539}
540
541async fn cancel_agent_task(
542    State(state): State<AppState>,
543    headers: HeaderMap,
544    Path(id): Path<String>,
545) -> Response {
546    let authority = match authenticate(&state, &headers).await {
547        Ok(value) => value,
548        Err(response) => return response,
549    };
550    let id = match parse_id(id) {
551        Ok(value) => value,
552        Err(error) => return invalid_task_id(error),
553    };
554    match state.link.cancel(&authority, &id).await {
555        Ok(value) => Json(AgentTask::from(value)).into_response(),
556        Err(error) => link_error(error),
557    }
558}
559
560async fn create_openai_response(
561    State(state): State<AppState>,
562    headers: HeaderMap,
563    payload: Result<Json<OpenAiResponseRequest>, JsonRejection>,
564) -> Response {
565    let authority = match authenticate(&state, &headers).await {
566        Ok(value) => value,
567        Err(_) => {
568            return openai_error(
569                StatusCode::UNAUTHORIZED,
570                "invalid_api_key",
571                "authentication failed",
572                None,
573            );
574        }
575    };
576    let Json(mut request) = match payload {
577        Ok(value) => value,
578        Err(error) => {
579            return openai_error(
580                StatusCode::BAD_REQUEST,
581                "invalid_json",
582                &error.body_text(),
583                None,
584            );
585        }
586    };
587    attach_request_id_metadata(&headers, &mut request.metadata);
588    if request.stream && !state.transport.sse {
589        return openai_error(
590            StatusCode::BAD_REQUEST,
591            "streaming_disabled",
592            "stream=true is disabled for this Runtime listener",
593            Some("stream"),
594        );
595    }
596    let execution_request = match openai_execution_request(&request) {
597        Ok(value) => value,
598        Err((param, message)) => {
599            return openai_error(
600                StatusCode::BAD_REQUEST,
601                "invalid_request_error",
602                &message,
603                param.as_deref(),
604            );
605        }
606    };
607    let key = idempotency_key_or_random(&headers, "openai-response");
608    let created = match state
609        .link
610        .create_execution(&authority, &key, execution_request)
611        .await
612    {
613        Ok(value) => value.execution,
614        Err(error) => return openai_link_error(error),
615    };
616    if request.stream {
617        let stream = openai_response_stream(
618            state.link,
619            authority,
620            created,
621            request.model,
622            request.instructions,
623            request.max_output_tokens,
624            request.metadata,
625        );
626        return Sse::new(stream)
627            .keep_alive(
628                KeepAlive::new()
629                    .interval(Duration::from_secs(15))
630                    .text("keepalive"),
631            )
632            .into_response();
633    }
634    if request.background {
635        return Json(openai_response_from_view(
636            created,
637            request.model,
638            request.instructions,
639            request.max_output_tokens,
640            request.metadata,
641        ))
642        .into_response();
643    }
644    match wait_for_provider_boundary(state.link, &authority, &created.id).await {
645        Ok(view) => Json(openai_response_from_view(
646            view,
647            request.model,
648            request.instructions,
649            request.max_output_tokens,
650            request.metadata,
651        ))
652        .into_response(),
653        Err(error) => openai_link_error(error),
654    }
655}
656
657async fn get_openai_response(
658    State(state): State<AppState>,
659    headers: HeaderMap,
660    Path(id): Path<String>,
661) -> Response {
662    let authority = match authenticate(&state, &headers).await {
663        Ok(value) => value,
664        Err(_) => {
665            return openai_error(
666                StatusCode::UNAUTHORIZED,
667                "invalid_api_key",
668                "authentication failed",
669                None,
670            );
671        }
672    };
673    let id = match parse_openai_response_id(id) {
674        Ok(value) => value,
675        Err(error) => {
676            return openai_error(
677                StatusCode::BAD_REQUEST,
678                "invalid_request_error",
679                &error.to_string(),
680                Some("response_id"),
681            );
682        }
683    };
684    match state.link.execution(&authority, &id).await {
685        Ok(view) => {
686            let model = view.runtime_instance_id.to_string();
687            Json(openai_response_from_view(
688                view,
689                model,
690                None,
691                None,
692                Default::default(),
693            ))
694            .into_response()
695        }
696        Err(error) => openai_link_error(error),
697    }
698}
699
700async fn cancel_openai_response(
701    State(state): State<AppState>,
702    headers: HeaderMap,
703    Path(id): Path<String>,
704) -> Response {
705    let authority = match authenticate(&state, &headers).await {
706        Ok(value) => value,
707        Err(_) => {
708            return openai_error(
709                StatusCode::UNAUTHORIZED,
710                "invalid_api_key",
711                "authentication failed",
712                None,
713            );
714        }
715    };
716    let id = match parse_openai_response_id(id) {
717        Ok(value) => value,
718        Err(error) => {
719            return openai_error(
720                StatusCode::BAD_REQUEST,
721                "invalid_request_error",
722                &error.to_string(),
723                Some("response_id"),
724            );
725        }
726    };
727    match state.link.cancel(&authority, &id).await {
728        Ok(view) => {
729            let model = view.runtime_instance_id.to_string();
730            Json(openai_response_from_view(
731                view,
732                model,
733                None,
734                None,
735                Default::default(),
736            ))
737            .into_response()
738        }
739        Err(error) => openai_link_error(error),
740    }
741}
742
743async fn create_claude_message(
744    State(state): State<AppState>,
745    headers: HeaderMap,
746    payload: Result<Json<ClaudeMessageRequest>, JsonRejection>,
747) -> Response {
748    let authority = match authenticate(&state, &headers).await {
749        Ok(value) => value,
750        Err(_) => {
751            return claude_error(
752                StatusCode::UNAUTHORIZED,
753                "authentication_error",
754                "authentication failed",
755            );
756        }
757    };
758    let anthropic_version = headers
759        .get("anthropic-version")
760        .and_then(|value| value.to_str().ok())
761        .filter(|value| !value.is_empty());
762    if anthropic_version.is_none() {
763        return claude_error(
764            StatusCode::BAD_REQUEST,
765            "invalid_request_error",
766            "anthropic-version header is required",
767        );
768    }
769    if anthropic_version != Some("2023-06-01") {
770        return claude_error(
771            StatusCode::BAD_REQUEST,
772            "invalid_request_error",
773            "unsupported anthropic-version; this listener supports 2023-06-01",
774        );
775    }
776    let Json(mut request) = match payload {
777        Ok(value) => value,
778        Err(error) => {
779            return claude_error(
780                StatusCode::BAD_REQUEST,
781                "invalid_request_error",
782                &error.body_text(),
783            );
784        }
785    };
786    if let Some(request_id) = request_id(&headers) {
787        request
788            .metadata
789            .entry("requestId".into())
790            .or_insert_with(|| serde_json::Value::String(request_id.to_string()));
791    }
792    if request.stream && !state.transport.sse {
793        return claude_error(
794            StatusCode::BAD_REQUEST,
795            "invalid_request_error",
796            "stream=true is disabled for this Runtime listener",
797        );
798    }
799    let execution_request = match claude_execution_request(&request) {
800        Ok(value) => value,
801        Err(message) => {
802            return claude_error(StatusCode::BAD_REQUEST, "invalid_request_error", &message);
803        }
804    };
805    let key = idempotency_key_or_random(&headers, "claude-message");
806    let created = match state
807        .link
808        .create_execution(&authority, &key, execution_request)
809        .await
810    {
811        Ok(value) => value.execution,
812        Err(error) => return claude_link_error(error),
813    };
814    if request.stream {
815        let stream = claude_message_stream(state.link, authority, created, request.model);
816        return Sse::new(stream)
817            .keep_alive(
818                KeepAlive::new()
819                    .interval(Duration::from_secs(15))
820                    .text("ping"),
821            )
822            .into_response();
823    }
824    match wait_for_provider_boundary(state.link, &authority, &created.id).await {
825        Ok(view) if view.state == ExecutionState::Completed => {
826            Json(claude_message_from_view(view, request.model)).into_response()
827        }
828        Ok(view) => claude_terminal_error(view),
829        Err(error) => claude_link_error(error),
830    }
831}
832
833fn openai_execution_request(
834    request: &OpenAiResponseRequest,
835) -> Result<CreateExecutionRequest, (Option<String>, String)> {
836    if let Some(field) = request.extra.keys().next() {
837        return Err((
838            Some(field.clone()),
839            format!("{field} is not supported by this Agent endpoint"),
840        ));
841    }
842    if !request.tools.is_empty() {
843        return Err((Some("tools".into()), "client-defined tools are not supported by the Agent endpoint; configure tools on the Agent Definition".into()));
844    }
845    for (present, field) in [
846        (
847            request.previous_response_id.is_some(),
848            "previous_response_id",
849        ),
850        (request.conversation.is_some(), "conversation"),
851        (request.reasoning.is_some(), "reasoning"),
852        (request.top_p.is_some(), "top_p"),
853        (!request.include.is_empty(), "include"),
854        (request.safety_identifier.is_some(), "safety_identifier"),
855        (request.prompt_cache_key.is_some(), "prompt_cache_key"),
856    ] {
857        if present {
858            return Err((
859                Some(field.into()),
860                format!("{field} cannot be preserved by this Agent endpoint"),
861            ));
862        }
863    }
864    if request.store == Some(true) {
865        return Err((
866            Some("store".into()),
867            "store=true requires a durable provider response store and is not supported".into(),
868        ));
869    }
870    if request
871        .truncation
872        .as_deref()
873        .is_some_and(|value| value != "disabled")
874    {
875        return Err((
876            Some("truncation".into()),
877            "only truncation=disabled is supported".into(),
878        ));
879    }
880    let messages =
881        extract_openai_input(&request.input).map_err(|message| (Some("input".into()), message))?;
882    let instructions = request
883        .instructions
884        .as_ref()
885        .map(extract_text_value)
886        .transpose()
887        .map_err(|message| (Some("instructions".into()), message))?;
888    let tool_choice = parse_openai_tool_choice(request.tool_choice.as_ref())
889        .map_err(|message| (Some("tool_choice".into()), message))?;
890    let response_format = request
891        .text
892        .as_ref()
893        .map(openai_response_format)
894        .transpose()
895        .map_err(|message| (Some("text".into()), message))?;
896    let agent = request.agent.as_ref();
897    if agent
898        .and_then(|value| value.conversation_id.as_ref())
899        .is_some()
900        && messages.len() > 1
901    {
902        return Err((
903            Some("agent.conversationId".into()),
904            "agent.conversationId cannot be combined with a multi-message stateless input history"
905                .into(),
906        ));
907    }
908    let runtime_instance_id = match agent.and_then(|value| value.runtime_id.clone()) {
909        Some(value) => value,
910        None => RuntimeInstanceId::new(request.model.clone())
911            .map_err(|error| (Some("model".into()), error.to_string()))?,
912    };
913    Ok(CreateExecutionRequest {
914        runtime_instance_id,
915        conversation_id: agent.and_then(|value| value.conversation_id.clone()),
916        input: RuntimeInput::Messages { messages },
917        workspace_id: agent.and_then(|value| value.workspace_id.clone()),
918        model: agent.and_then(|value| value.downstream_model.clone()),
919        instructions,
920        metadata: request.metadata.clone(),
921        generation: ModelGenerationOptions {
922            max_output_tokens: request.max_output_tokens,
923            temperature: request.temperature.map(|value| value as f32),
924            stop_sequences: Vec::new(),
925            response_format,
926            tool_choice,
927        },
928        options: ExecutionOptions {
929            deadline_seconds: agent
930                .and_then(|value| value.deadline_seconds)
931                .unwrap_or(900),
932            max_model_turns: agent.and_then(|value| value.max_model_turns).unwrap_or(64),
933            max_tool_calls: agent.and_then(|value| value.max_tool_calls).unwrap_or(256),
934        },
935    })
936}
937
938fn claude_execution_request(
939    request: &ClaudeMessageRequest,
940) -> Result<CreateExecutionRequest, String> {
941    if let Some(field) = request.extra.keys().next() {
942        return Err(format!("{field} is not supported by this Agent endpoint"));
943    }
944    if request.max_tokens == 0 {
945        return Err("max_tokens must be greater than zero".into());
946    }
947    if request
948        .temperature
949        .is_some_and(|value| !value.is_finite() || !(0.0..=1.0).contains(&value))
950    {
951        return Err("temperature must be within 0..=1 for Claude Messages".into());
952    }
953    if request.messages.is_empty() {
954        return Err("messages must not be empty".into());
955    }
956    if !request.tools.is_empty() {
957        return Err("client-defined tools are not supported by the Agent endpoint; configure tools on the Agent Definition".into());
958    }
959    if request.top_p.is_some() || request.top_k.is_some() {
960        return Err("top_p and top_k cannot be preserved by this Agent endpoint".into());
961    }
962    if request.thinking.is_some()
963        || request.output_config.is_some()
964        || request.service_tier.is_some()
965    {
966        return Err(
967            "thinking, output_config, and service_tier cannot be preserved by this Agent endpoint"
968                .into(),
969        );
970    }
971    let messages = extract_claude_messages(&request.messages)?;
972    let instructions = request
973        .system
974        .as_ref()
975        .map(extract_text_value)
976        .transpose()?;
977    let tool_choice = parse_claude_tool_choice(request.tool_choice.as_ref())?;
978    let metadata = request
979        .metadata
980        .iter()
981        .map(|(key, value)| (key.clone(), scalar_metadata(value)))
982        .collect();
983    let agent = request.agent.as_ref();
984    if agent
985        .and_then(|value| value.conversation_id.as_ref())
986        .is_some()
987        && messages.len() > 1
988    {
989        return Err(
990            "agent.conversationId cannot be combined with a multi-message stateless history".into(),
991        );
992    }
993    let runtime_instance_id = match agent.and_then(|value| value.runtime_id.clone()) {
994        Some(value) => value,
995        None => RuntimeInstanceId::new(request.model.clone()).map_err(|error| error.to_string())?,
996    };
997    Ok(CreateExecutionRequest {
998        runtime_instance_id,
999        conversation_id: agent.and_then(|value| value.conversation_id.clone()),
1000        input: RuntimeInput::Messages { messages },
1001        workspace_id: agent.and_then(|value| value.workspace_id.clone()),
1002        model: agent.and_then(|value| value.downstream_model.clone()),
1003        instructions,
1004        metadata,
1005        generation: ModelGenerationOptions {
1006            max_output_tokens: Some(request.max_tokens),
1007            temperature: request.temperature.map(|value| value as f32),
1008            stop_sequences: request.stop_sequences.clone(),
1009            response_format: None,
1010            tool_choice,
1011        },
1012        options: ExecutionOptions {
1013            deadline_seconds: agent
1014                .and_then(|value| value.deadline_seconds)
1015                .unwrap_or(900),
1016            max_model_turns: agent.and_then(|value| value.max_model_turns).unwrap_or(64),
1017            max_tool_calls: agent.and_then(|value| value.max_tool_calls).unwrap_or(256),
1018        },
1019    })
1020}
1021
1022fn parse_openai_tool_choice(value: Option<&serde_json::Value>) -> Result<ModelToolChoice, String> {
1023    let Some(value) = value else {
1024        return Ok(ModelToolChoice::Auto);
1025    };
1026    match value.as_str() {
1027        Some("auto") => Ok(ModelToolChoice::Auto),
1028        Some("none") => Ok(ModelToolChoice::None),
1029        _ => Err(
1030            "only tool_choice=auto or none can be preserved across Agent Infra providers".into(),
1031        ),
1032    }
1033}
1034
1035fn parse_claude_tool_choice(value: Option<&serde_json::Value>) -> Result<ModelToolChoice, String> {
1036    let Some(value) = value else {
1037        return Ok(ModelToolChoice::Auto);
1038    };
1039    let object = value
1040        .as_object()
1041        .ok_or_else(|| "tool_choice must be an object".to_string())?;
1042    if object.len() != 1 {
1043        return Err("tool_choice contains fields this Agent endpoint cannot preserve".into());
1044    }
1045    match value.get("type").and_then(serde_json::Value::as_str) {
1046        Some("auto") => Ok(ModelToolChoice::Auto),
1047        Some("none") => Ok(ModelToolChoice::None),
1048        _ => Err(
1049            "only tool_choice.type auto or none can be preserved across Agent Infra providers"
1050                .into(),
1051        ),
1052    }
1053}
1054
1055fn openai_response_format(text: &serde_json::Value) -> Result<serde_json::Value, String> {
1056    let format = text
1057        .get("format")
1058        .ok_or_else(|| "text.format is required when text is supplied".to_string())?;
1059    if !format.is_object() {
1060        return Err("text.format must be an object".into());
1061    }
1062    match format.get("type").and_then(serde_json::Value::as_str) {
1063        Some("json_schema") => {
1064            let object = format.as_object().expect("format was checked as an object");
1065            let name = object
1066                .get("name")
1067                .and_then(serde_json::Value::as_str)
1068                .filter(|name| !name.trim().is_empty())
1069                .ok_or_else(|| "text.format json_schema requires name".to_string())?;
1070            let schema = object
1071                .get("schema")
1072                .filter(|schema| schema.is_object())
1073                .ok_or_else(|| "text.format json_schema requires an object schema".to_string())?;
1074            let mut json_schema = serde_json::Map::new();
1075            json_schema.insert("name".into(), json!(name));
1076            json_schema.insert("schema".into(), schema.clone());
1077            for field in ["description", "strict"] {
1078                if let Some(value) = object.get(field) {
1079                    json_schema.insert(field.into(), value.clone());
1080                }
1081            }
1082            Ok(json!({"type":"json_schema","json_schema":json_schema}))
1083        }
1084        Some("text" | "json_object") => Ok(format.clone()),
1085        _ => Err("text.format.type must be text, json_object, or json_schema".into()),
1086    }
1087}
1088
1089fn extract_openai_input(input: &serde_json::Value) -> Result<Vec<RuntimeMessage>, String> {
1090    if let Some(text) = input.as_str() {
1091        return Ok(vec![RuntimeMessage {
1092            role: RuntimeMessageRole::User,
1093            text: non_empty_text(text)?,
1094        }]);
1095    }
1096    let items = input
1097        .as_array()
1098        .ok_or_else(|| "input must be a string or an array of message items".to_string())?;
1099    let mut messages = Vec::new();
1100    for item in items {
1101        let object = item
1102            .as_object()
1103            .ok_or_else(|| "input items must be objects".to_string())?;
1104        if let Some(field) = object.keys().find(|field| {
1105            !matches!(
1106                field.as_str(),
1107                "type" | "role" | "content" | "id" | "status"
1108            )
1109        }) {
1110            return Err(format!("input message field {field} is not supported"));
1111        }
1112        let item_type = object
1113            .get("type")
1114            .and_then(|value| value.as_str())
1115            .unwrap_or("message");
1116        if item_type != "message" {
1117            return Err(format!("input item type {item_type} is not supported"));
1118        }
1119        let role = object
1120            .get("role")
1121            .and_then(|value| value.as_str())
1122            .unwrap_or("user");
1123        let role = match role {
1124            "system" => RuntimeMessageRole::System,
1125            "developer" => RuntimeMessageRole::Developer,
1126            "user" => RuntimeMessageRole::User,
1127            "assistant" => RuntimeMessageRole::Assistant,
1128            role => return Err(format!("input message role {role} is not supported")),
1129        };
1130        let content = object
1131            .get("content")
1132            .ok_or_else(|| "message content is required".to_string())?;
1133        messages.push(RuntimeMessage {
1134            role,
1135            text: extract_text_value(content)?,
1136        });
1137    }
1138    if messages.is_empty() {
1139        Err("input message array must not be empty".into())
1140    } else {
1141        Ok(messages)
1142    }
1143}
1144
1145fn extract_claude_messages(
1146    messages: &[runtime_api_contract::ClaudeInputMessage],
1147) -> Result<Vec<RuntimeMessage>, String> {
1148    let mut transcript = Vec::with_capacity(messages.len());
1149    for message in messages {
1150        if message.role != "user" && message.role != "assistant" {
1151            return Err("Claude message role must be user or assistant".into());
1152        }
1153        transcript.push(RuntimeMessage {
1154            role: if message.role == "user" {
1155                RuntimeMessageRole::User
1156            } else {
1157                RuntimeMessageRole::Assistant
1158            },
1159            text: extract_text_value(&message.content)?,
1160        });
1161    }
1162    Ok(transcript)
1163}
1164
1165fn extract_text_value(value: &serde_json::Value) -> Result<String, String> {
1166    if let Some(text) = value.as_str() {
1167        return non_empty_text(text);
1168    }
1169    let blocks = value
1170        .as_array()
1171        .ok_or_else(|| "content must be text or an array of text blocks".to_string())?;
1172    let mut text = Vec::new();
1173    for block in blocks {
1174        let object = block
1175            .as_object()
1176            .ok_or_else(|| "content blocks must be objects".to_string())?;
1177        if let Some(field) = object
1178            .keys()
1179            .find(|field| !matches!(field.as_str(), "type" | "text"))
1180        {
1181            return Err(format!("content block field {field} is not supported"));
1182        }
1183        let kind = object
1184            .get("type")
1185            .and_then(|value| value.as_str())
1186            .unwrap_or("text");
1187        if kind != "text" && kind != "input_text" && kind != "output_text" {
1188            return Err(format!("content block type {kind} is not supported"));
1189        }
1190        let part = object
1191            .get("text")
1192            .and_then(|value| value.as_str())
1193            .ok_or_else(|| "text block requires text".to_string())?;
1194        text.push(part);
1195    }
1196    non_empty_text(&text.join("\n"))
1197}
1198
1199fn non_empty_text(text: &str) -> Result<String, String> {
1200    if text.trim().is_empty() {
1201        Err("text content must not be empty".into())
1202    } else {
1203        Ok(text.to_string())
1204    }
1205}
1206
1207fn scalar_metadata(value: &serde_json::Value) -> String {
1208    value
1209        .as_str()
1210        .map(ToOwned::to_owned)
1211        .unwrap_or_else(|| value.to_string())
1212}
1213
1214/// Provider request/stream calls must not hang forever when an Agent pauses for
1215/// human input. WaitingForInput is therefore a response boundary even though
1216/// the native Task itself remains resumable and non-terminal.
1217async fn wait_for_provider_boundary(
1218    link: Arc<dyn RuntimeLink>,
1219    authority: &RequestAuthority,
1220    id: &ExecutionId,
1221) -> Result<runtime_api_contract::ExecutionView, LinkError> {
1222    loop {
1223        let view = link.execution(authority, id).await?;
1224        if view.state.is_terminal() || view.state == ExecutionState::WaitingForInput {
1225            return Ok(view);
1226        }
1227        tokio::time::sleep(Duration::from_millis(100)).await;
1228    }
1229}
1230
1231fn openai_response_from_view(
1232    view: runtime_api_contract::ExecutionView,
1233    model: String,
1234    instructions: Option<serde_json::Value>,
1235    max_output_tokens: Option<u32>,
1236    metadata: std::collections::BTreeMap<String, String>,
1237) -> OpenAiResponse {
1238    let status = match view.state {
1239        ExecutionState::Queued | ExecutionState::Running | ExecutionState::Finalizing => {
1240            "in_progress"
1241        }
1242        ExecutionState::WaitingForInput => "incomplete",
1243        ExecutionState::Completed => "completed",
1244        ExecutionState::Failed => "failed",
1245        ExecutionState::Canceled => "cancelled",
1246    };
1247    let output = view
1248        .outcome
1249        .as_ref()
1250        .map(|outcome| {
1251            vec![OpenAiOutputItem {
1252                id: format!("msg_{}", view.id),
1253                item_type: "message".into(),
1254                status: "completed".into(),
1255                role: "assistant".into(),
1256                content: vec![OpenAiOutputContent {
1257                    content_type: "output_text".into(),
1258                    text: outcome.answer.clone(),
1259                    annotations: Vec::new(),
1260                }],
1261            }]
1262        })
1263        .unwrap_or_default();
1264    let usage = view.outcome.as_ref().map(|outcome| {
1265        let input = outcome.input_tokens.unwrap_or_default();
1266        let output = outcome.output_tokens.unwrap_or_default();
1267        OpenAiUsage {
1268            input_tokens: input,
1269            input_tokens_details: OpenAiInputTokensDetails { cached_tokens: 0 },
1270            output_tokens: output,
1271            output_tokens_details: OpenAiOutputTokensDetails {
1272                reasoning_tokens: 0,
1273            },
1274            total_tokens: input.saturating_add(output),
1275        }
1276    });
1277    let error = view.failure.as_ref().map(|failure| OpenAiResponseError {
1278        code: failure.code.clone(),
1279        message: failure.message.clone(),
1280    });
1281    OpenAiResponse {
1282        id: format!("resp_{}", view.id),
1283        object: "response".into(),
1284        created_at: view.created_at_ms / 1000,
1285        status: status.into(),
1286        completed_at: view
1287            .state
1288            .is_terminal()
1289            .then_some(view.updated_at_ms / 1000),
1290        error,
1291        incomplete_details: (view.state == ExecutionState::WaitingForInput)
1292            .then(|| json!({"reason":"requires_input"})),
1293        instructions,
1294        max_output_tokens,
1295        model,
1296        output,
1297        parallel_tool_calls: false,
1298        previous_response_id: None,
1299        tool_choice: json!("none"),
1300        tools: Vec::new(),
1301        metadata,
1302        usage,
1303    }
1304}
1305
1306fn claude_message_from_view(
1307    view: runtime_api_contract::ExecutionView,
1308    model: String,
1309) -> ClaudeMessage {
1310    let outcome = view.outcome.expect("completed execution has an outcome");
1311    ClaudeMessage {
1312        id: format!("msg_{}", view.id),
1313        message_type: "message".into(),
1314        role: "assistant".into(),
1315        content: vec![ClaudeContentBlock::Text {
1316            text: outcome.answer,
1317        }],
1318        model,
1319        stop_reason: Some("end_turn".into()),
1320        stop_sequence: None,
1321        usage: ClaudeUsage {
1322            input_tokens: outcome.input_tokens.unwrap_or_default(),
1323            output_tokens: outcome.output_tokens.unwrap_or_default(),
1324        },
1325    }
1326}
1327
1328fn openai_response_stream(
1329    link: Arc<dyn RuntimeLink>,
1330    authority: RequestAuthority,
1331    initial: runtime_api_contract::ExecutionView,
1332    model: String,
1333    instructions: Option<serde_json::Value>,
1334    max_output_tokens: Option<u32>,
1335    metadata: std::collections::BTreeMap<String, String>,
1336) -> impl Stream<Item = Result<Event, Infallible>> {
1337    async_stream::stream! {
1338        let created = openai_response_from_view(initial.clone(), model.clone(), instructions.clone(), max_output_tokens, metadata.clone());
1339        yield Ok(sse_json("response.created", json!({"type":"response.created","response":created,"sequence_number":0})));
1340        let final_view = wait_for_provider_boundary(link, &authority, &initial.id).await;
1341        match final_view {
1342            Ok(view) if view.state == ExecutionState::Completed => {
1343                let response = openai_response_from_view(view, model, instructions, max_output_tokens, metadata);
1344                let item = response.output.first().cloned().expect("completed response has output");
1345                let text = item.content.first().map(|content| content.text.clone()).unwrap_or_default();
1346                yield Ok(sse_json("response.output_item.added", json!({"type":"response.output_item.added","output_index":0,"item":{"id":item.id,"type":"message","status":"in_progress","role":"assistant","content":[]},"sequence_number":1})));
1347                yield Ok(sse_json("response.content_part.added", json!({"type":"response.content_part.added","item_id":item.id,"output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]},"sequence_number":2})));
1348                yield Ok(sse_json("response.output_text.delta", json!({"type":"response.output_text.delta","item_id":item.id,"output_index":0,"content_index":0,"delta":text,"logprobs":[],"sequence_number":3})));
1349                yield Ok(sse_json("response.output_text.done", json!({"type":"response.output_text.done","item_id":item.id,"output_index":0,"content_index":0,"text":text,"logprobs":[],"sequence_number":4})));
1350                yield Ok(sse_json("response.content_part.done", json!({"type":"response.content_part.done","item_id":item.id,"output_index":0,"content_index":0,"part":item.content[0],"sequence_number":5})));
1351                yield Ok(sse_json("response.output_item.done", json!({"type":"response.output_item.done","output_index":0,"item":item,"sequence_number":6})));
1352                yield Ok(sse_json("response.completed", json!({"type":"response.completed","response":response,"sequence_number":7})));
1353            }
1354            Ok(view) => {
1355                let event_type = match view.state {
1356                    ExecutionState::Canceled => "response.cancelled",
1357                    ExecutionState::WaitingForInput => "response.incomplete",
1358                    _ => "response.failed",
1359                };
1360                let response = openai_response_from_view(view, model, instructions, max_output_tokens, metadata);
1361                yield Ok(sse_json(event_type, json!({"type":event_type,"response":response,"sequence_number":1})));
1362            }
1363            Err(error) => {
1364                yield Ok(sse_json("error", json!({"type":"error","error":{"type":"api_error","code":"runtime_unavailable","message":error.to_string()}})));
1365            }
1366        }
1367    }
1368}
1369
1370fn claude_message_stream(
1371    link: Arc<dyn RuntimeLink>,
1372    authority: RequestAuthority,
1373    initial: runtime_api_contract::ExecutionView,
1374    model: String,
1375) -> impl Stream<Item = Result<Event, Infallible>> {
1376    async_stream::stream! {
1377        yield Ok(sse_json("message_start", json!({
1378            "type":"message_start",
1379            "message":{"id":format!("msg_{}", initial.id),"type":"message","role":"assistant","content":[],"model":model,"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}
1380        })));
1381        match wait_for_provider_boundary(link, &authority, &initial.id).await {
1382            Ok(view) if view.state == ExecutionState::Completed => {
1383                let message = claude_message_from_view(view, model);
1384                let text = match &message.content[0] { ClaudeContentBlock::Text { text } => text.clone() };
1385                yield Ok(sse_json("content_block_start", json!({"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}})));
1386                yield Ok(sse_json("content_block_delta", json!({"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":text}})));
1387                yield Ok(sse_json("content_block_stop", json!({"type":"content_block_stop","index":0})));
1388                yield Ok(sse_json("message_delta", json!({"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":message.usage.output_tokens}})));
1389                yield Ok(sse_json("message_stop", json!({"type":"message_stop"})));
1390            }
1391            Ok(view) => {
1392                let message = view.failure.map(|failure| failure.message).unwrap_or_else(|| "Agent Task was canceled".into());
1393                yield Ok(sse_json("error", json!({"type":"error","error":{"type":"api_error","message":message}})));
1394            }
1395            Err(error) => {
1396                yield Ok(sse_json("error", json!({"type":"error","error":{"type":"api_error","message":error.to_string()}})));
1397            }
1398        }
1399    }
1400}
1401
1402fn sse_json(event_type: &str, value: serde_json::Value) -> Event {
1403    Event::default()
1404        .event(event_type)
1405        .json_data(value)
1406        .unwrap_or_else(|_| {
1407            Event::default()
1408                .event("error")
1409                .data("serialization failure")
1410        })
1411}
1412
1413fn openai_error(status: StatusCode, code: &str, message: &str, param: Option<&str>) -> Response {
1414    (
1415        status,
1416        Json(OpenAiErrorEnvelope {
1417            error: OpenAiError {
1418                message: message.into(),
1419                error_type: if status == StatusCode::UNAUTHORIZED {
1420                    "authentication_error".into()
1421                } else {
1422                    "invalid_request_error".into()
1423                },
1424                param: param.map(ToOwned::to_owned),
1425                code: Some(code.into()),
1426            },
1427        }),
1428    )
1429        .into_response()
1430}
1431
1432fn openai_link_error(error: LinkError) -> Response {
1433    match error {
1434        LinkError::Invalid(message) => {
1435            openai_error(StatusCode::BAD_REQUEST, "invalid_request", &message, None)
1436        }
1437        LinkError::NotFound => openai_error(
1438            StatusCode::NOT_FOUND,
1439            "not_found",
1440            "response not found",
1441            None,
1442        ),
1443        LinkError::Forbidden => openai_error(
1444            StatusCode::FORBIDDEN,
1445            "permission_denied",
1446            "caller cannot access this response",
1447            None,
1448        ),
1449        LinkError::Conflict(message) => {
1450            openai_error(StatusCode::CONFLICT, "conflict", &message, None)
1451        }
1452        LinkError::Overloaded => openai_error(
1453            StatusCode::TOO_MANY_REQUESTS,
1454            "rate_limit_exceeded",
1455            "runtime is at capacity",
1456            None,
1457        ),
1458        LinkError::Unavailable(message) | LinkError::Internal(message) => openai_error(
1459            StatusCode::SERVICE_UNAVAILABLE,
1460            "server_error",
1461            &message,
1462            None,
1463        ),
1464    }
1465}
1466
1467fn claude_error(status: StatusCode, error_type: &str, message: &str) -> Response {
1468    (
1469        status,
1470        Json(ClaudeErrorEnvelope {
1471            envelope_type: "error".into(),
1472            error: ClaudeError {
1473                error_type: error_type.into(),
1474                message: message.into(),
1475            },
1476            request_id: None,
1477        }),
1478    )
1479        .into_response()
1480}
1481
1482fn claude_link_error(error: LinkError) -> Response {
1483    match error {
1484        LinkError::Invalid(message) => {
1485            claude_error(StatusCode::BAD_REQUEST, "invalid_request_error", &message)
1486        }
1487        LinkError::NotFound => claude_error(
1488            StatusCode::NOT_FOUND,
1489            "not_found_error",
1490            "message task not found",
1491        ),
1492        LinkError::Forbidden => claude_error(
1493            StatusCode::FORBIDDEN,
1494            "permission_error",
1495            "caller cannot access this message task",
1496        ),
1497        LinkError::Overloaded => claude_error(
1498            StatusCode::TOO_MANY_REQUESTS,
1499            "rate_limit_error",
1500            "runtime is at capacity",
1501        ),
1502        LinkError::Conflict(message) => {
1503            claude_error(StatusCode::CONFLICT, "invalid_request_error", &message)
1504        }
1505        LinkError::Unavailable(message) | LinkError::Internal(message) => {
1506            claude_error(StatusCode::SERVICE_UNAVAILABLE, "api_error", &message)
1507        }
1508    }
1509}
1510
1511fn claude_terminal_error(view: runtime_api_contract::ExecutionView) -> Response {
1512    let message = view
1513        .failure
1514        .map(|failure| failure.message)
1515        .unwrap_or_else(|| "Agent Task was canceled".into());
1516    claude_error(StatusCode::INTERNAL_SERVER_ERROR, "api_error", &message)
1517}
1518
1519#[derive(Debug, Deserialize)]
1520#[serde(rename_all = "camelCase", deny_unknown_fields)]
1521struct EventQuery {
1522    after: Option<u64>,
1523    limit: Option<usize>,
1524}
1525
1526async fn websocket_agent_task(
1527    upgrade: WebSocketUpgrade,
1528    State(state): State<AppState>,
1529    headers: HeaderMap,
1530    Path(id): Path<String>,
1531    Query(query): Query<EventQuery>,
1532) -> Response {
1533    let authority = match authenticate(&state, &headers).await {
1534        Ok(value) => value,
1535        Err(response) => return response,
1536    };
1537    let id = match parse_id(id) {
1538        Ok(value) => value,
1539        Err(error) => return invalid_task_id(error),
1540    };
1541    let task = match state.link.execution(&authority, &id).await {
1542        Ok(value) => AgentTask::from(value),
1543        Err(error) => return link_error(error),
1544    };
1545    upgrade
1546        .on_upgrade(move |socket| {
1547            websocket_agent_task_session(socket, state.link, authority, id, task, query.after)
1548        })
1549        .into_response()
1550}
1551
1552async fn websocket_agent_task_session(
1553    mut socket: WebSocket,
1554    link: Arc<dyn RuntimeLink>,
1555    authority: RequestAuthority,
1556    id: ExecutionId,
1557    task: AgentTask,
1558    mut after: Option<u64>,
1559) {
1560    if send_agent_task_websocket(
1561        &mut socket,
1562        &AgentTaskWebSocketServerMessage::Connected {
1563            task,
1564            next_after: after,
1565            heartbeat_seconds: 15,
1566        },
1567    )
1568    .await
1569    .is_err()
1570    {
1571        return;
1572    }
1573
1574    let mut poll = tokio::time::interval(Duration::from_millis(100));
1575    poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1576    let mut heartbeat = tokio::time::interval(Duration::from_secs(15));
1577    heartbeat.tick().await;
1578    loop {
1579        tokio::select! {
1580            incoming = socket.next() => {
1581                match incoming {
1582                    Some(Ok(Message::Text(text))) => {
1583                        let command = match serde_json::from_str::<AgentTaskWebSocketClientMessage>(&text) {
1584                            Ok(command) => command,
1585                            Err(_) => {
1586                                let _ = send_agent_task_websocket_error(&mut socket, "WEBSOCKET_MESSAGE_INVALID", "Agent Task WebSocket command is invalid").await;
1587                                continue;
1588                            }
1589                        };
1590                        let result = match command {
1591                            AgentTaskWebSocketClientMessage::SubmitInput { operation_id, input } => {
1592                                Some(link.submit_input(&authority, &id, SubmitInputRequest { operation_id, input }).await)
1593                            }
1594                            AgentTaskWebSocketClientMessage::Cancel {} => {
1595                                Some(link.cancel(&authority, &id).await)
1596                            }
1597                            AgentTaskWebSocketClientMessage::Ack { sequence } => {
1598                                // Acknowledgements are monotonic client resume hints. Never
1599                                // move the delivery cursor backwards or skip unsent events.
1600                                if after.is_none_or(|delivered| sequence > delivered) {
1601                                    let _ = send_agent_task_websocket_error(&mut socket, "ACK_SEQUENCE_INVALID", "cannot acknowledge an event that has not been delivered").await;
1602                                } else if send_agent_task_websocket(
1603                                    &mut socket,
1604                                    &AgentTaskWebSocketServerMessage::Acknowledged { sequence },
1605                                ).await.is_err() {
1606                                    return;
1607                                }
1608                                None
1609                            }
1610                        };
1611                        if let Some(result) = result {
1612                            match result {
1613                                Ok(view) => {
1614                                    if send_agent_task_websocket(&mut socket, &AgentTaskWebSocketServerMessage::Task { task: AgentTask::from(view) }).await.is_err() {
1615                                        return;
1616                                    }
1617                                }
1618                                Err(_) => {
1619                                    if send_agent_task_websocket_error(&mut socket, "WEBSOCKET_COMMAND_FAILED", "Agent Task command could not be committed").await.is_err() {
1620                                        return;
1621                                    }
1622                                }
1623                            }
1624                        }
1625                    }
1626                    Some(Ok(Message::Ping(value))) => {
1627                        if socket.send(Message::Pong(value)).await.is_err() { return; }
1628                    }
1629                    Some(Ok(Message::Close(_))) | None | Some(Err(_)) => return,
1630                    Some(Ok(Message::Binary(_))) | Some(Ok(Message::Pong(_))) => {}
1631                }
1632            }
1633            _ = poll.tick() => {
1634                let page = match link.events(&authority, &id, after, 100).await {
1635                    Ok(page) => page,
1636                    Err(_) => {
1637                        let _ = send_agent_task_websocket_error(&mut socket, "WEBSOCKET_EVENTS_UNAVAILABLE", "Agent Task events are unavailable").await;
1638                        return;
1639                    }
1640                };
1641                let empty = page.items.is_empty();
1642                for item in page.items {
1643                    after = Some(item.sequence);
1644                    if send_agent_task_websocket(
1645                        &mut socket,
1646                        &AgentTaskWebSocketServerMessage::Event { event: AgentTaskEvent::from(item) },
1647                    ).await.is_err() {
1648                        return;
1649                    }
1650                }
1651                if empty {
1652                    match link.execution(&authority, &id).await {
1653                        Ok(view) if view.state.is_terminal() => {
1654                            let _ = send_agent_task_websocket(&mut socket, &AgentTaskWebSocketServerMessage::Task { task: AgentTask::from(view) }).await;
1655                            let _ = socket.close().await;
1656                            return;
1657                        }
1658                        Err(_) => return,
1659                        _ => {}
1660                    }
1661                }
1662            }
1663            _ = heartbeat.tick() => {
1664                if socket.send(Message::Ping(Vec::new().into())).await.is_err() {
1665                    return;
1666                }
1667            }
1668        }
1669    }
1670}
1671
1672async fn send_agent_task_websocket(
1673    socket: &mut WebSocket,
1674    message: &AgentTaskWebSocketServerMessage,
1675) -> Result<(), axum::Error> {
1676    let json = serde_json::to_string(message).map_err(axum::Error::new)?;
1677    socket.send(Message::Text(json.into())).await
1678}
1679
1680async fn send_agent_task_websocket_error(
1681    socket: &mut WebSocket,
1682    code: &str,
1683    message: &str,
1684) -> Result<(), axum::Error> {
1685    send_agent_task_websocket(
1686        socket,
1687        &AgentTaskWebSocketServerMessage::Error {
1688            error: ApiErrorBody {
1689                code: code.into(),
1690                message: message.into(),
1691                request_id: None,
1692                details: None,
1693            },
1694        },
1695    )
1696    .await
1697}
1698
1699fn agent_task_event_stream(
1700    link: Arc<dyn RuntimeLink>,
1701    authority: RequestAuthority,
1702    id: ExecutionId,
1703    initial_after: Option<u64>,
1704) -> impl Stream<Item = Result<Event, Infallible>> {
1705    async_stream::stream! {
1706        let mut after = initial_after;
1707        loop {
1708            match link.events(&authority, &id, after, 100).await {
1709                Ok(page) => {
1710                    let empty = page.items.is_empty();
1711                    for item in page.items {
1712                        after = Some(item.sequence);
1713                        let event = AgentTaskEvent::from(item);
1714                        let frame = Event::default()
1715                            .id(event.sequence.to_string())
1716                            .event(event.kind.clone())
1717                            .json_data(&event)
1718                            .unwrap_or_else(|_| Event::default().event("task.error").data("serialization failure"));
1719                        yield Ok(frame);
1720                    }
1721                    if empty {
1722                        match link.execution(&authority, &id).await {
1723                            Ok(view) if view.state.is_terminal() => break,
1724                            Err(_) => break,
1725                            _ => {}
1726                        }
1727                    }
1728                }
1729                Err(error) => {
1730                    yield Ok(Event::default().event("task.error").data(error.to_string()));
1731                    break;
1732                }
1733            }
1734            tokio::time::sleep(Duration::from_millis(100)).await;
1735        }
1736    }
1737}
1738
1739fn idempotency_key_or_random(headers: &HeaderMap, prefix: &str) -> String {
1740    headers
1741        .get("idempotency-key")
1742        .and_then(|value| value.to_str().ok())
1743        .filter(|value| !value.is_empty())
1744        .map(ToOwned::to_owned)
1745        .unwrap_or_else(|| format!("{prefix}:{}", uuid::Uuid::new_v4()))
1746}
1747
1748fn request_id(headers: &HeaderMap) -> Option<&str> {
1749    headers
1750        .get("x-request-id")
1751        .and_then(|value| value.to_str().ok())
1752}
1753
1754fn sse_resume_after(headers: &HeaderMap, query_after: Option<u64>) -> Result<Option<u64>, String> {
1755    let header_after = headers
1756        .get("last-event-id")
1757        .map(|value| {
1758            value
1759                .to_str()
1760                .map_err(|_| "Last-Event-ID must be ASCII".to_string())?
1761                .parse::<u64>()
1762                .map_err(|_| "Last-Event-ID must be an unsigned integer".to_string())
1763        })
1764        .transpose()?;
1765    if query_after.is_some() && header_after.is_some() && query_after != header_after {
1766        return Err("after and Last-Event-ID must identify the same sequence".into());
1767    }
1768    Ok(query_after.or(header_after))
1769}
1770
1771fn attach_request_id_metadata(
1772    headers: &HeaderMap,
1773    metadata: &mut std::collections::BTreeMap<String, String>,
1774) {
1775    if let Some(request_id) = request_id(headers) {
1776        metadata
1777            .entry("requestId".into())
1778            .or_insert_with(|| request_id.to_string());
1779    }
1780}
1781
1782fn invalid_task_id(error: runtime_types::IdentifierError) -> Response {
1783    api_error(
1784        StatusCode::BAD_REQUEST,
1785        "TASK_ID_INVALID",
1786        &error.to_string(),
1787    )
1788}
1789
1790async fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<RequestAuthority, Response> {
1791    state
1792        .authenticator
1793        .authenticate(headers)
1794        .await
1795        .map_err(|_| {
1796            api_error(
1797                StatusCode::UNAUTHORIZED,
1798                "UNAUTHENTICATED",
1799                "request authentication failed",
1800            )
1801        })
1802}
1803
1804fn parse_id(value: String) -> Result<ExecutionId, runtime_types::IdentifierError> {
1805    ExecutionId::new(value)
1806}
1807
1808fn parse_openai_response_id(value: String) -> Result<ExecutionId, runtime_types::IdentifierError> {
1809    ExecutionId::new(value.strip_prefix("resp_").unwrap_or(&value).to_string())
1810}
1811
1812fn link_error(error: LinkError) -> Response {
1813    match error {
1814        LinkError::Invalid(message) => {
1815            api_error(StatusCode::BAD_REQUEST, "INVALID_REQUEST", &message)
1816        }
1817        LinkError::NotFound => api_error(
1818            StatusCode::NOT_FOUND,
1819            "EXECUTION_NOT_FOUND",
1820            "execution not found",
1821        ),
1822        LinkError::Forbidden => api_error(
1823            StatusCode::FORBIDDEN,
1824            "FORBIDDEN",
1825            "caller cannot access this execution",
1826        ),
1827        LinkError::Conflict(message) => {
1828            api_error(StatusCode::CONFLICT, "EXECUTION_CONFLICT", &message)
1829        }
1830        LinkError::Overloaded => api_error(
1831            StatusCode::TOO_MANY_REQUESTS,
1832            "RUNTIME_OVERLOADED",
1833            "runtime is at capacity",
1834        ),
1835        LinkError::Unavailable(message) => api_error(
1836            StatusCode::SERVICE_UNAVAILABLE,
1837            "DEPENDENCY_UNAVAILABLE",
1838            &message,
1839        ),
1840        LinkError::Internal(message) => {
1841            tracing::error!(%message, "runtime internal error");
1842            api_error(
1843                StatusCode::INTERNAL_SERVER_ERROR,
1844                "INTERNAL",
1845                "runtime internal failure",
1846            )
1847        }
1848    }
1849}
1850
1851fn api_error(status: StatusCode, code: &str, message: &str) -> Response {
1852    (
1853        status,
1854        Json(ApiErrorBody {
1855            code: code.into(),
1856            message: message.into(),
1857            request_id: None,
1858            details: None,
1859        }),
1860    )
1861        .into_response()
1862}
1863
1864#[cfg(test)]
1865mod tests {
1866    use std::sync::Arc;
1867
1868    use axum::{body::Body, http::Request};
1869    use http_body_util::BodyExt;
1870    use runtime_api_contract::{CreateExecutionResponse, EventPage, ExecutionState, ExecutionView};
1871    use runtime_types::RuntimeInstanceId;
1872    use tokio_tungstenite::{
1873        connect_async,
1874        tungstenite::{Message as ClientMessage, client::IntoClientRequest},
1875    };
1876    use tower::ServiceExt;
1877
1878    use super::*;
1879
1880    struct FakeLink {
1881        terminal: bool,
1882    }
1883
1884    fn view(terminal: bool) -> ExecutionView {
1885        ExecutionView {
1886            id: ExecutionId::new("execution-1").unwrap(),
1887            runtime_instance_id: RuntimeInstanceId::new("runtime-1").unwrap(),
1888            conversation_id: None,
1889            workspace_id: None,
1890            model: None,
1891            metadata: Default::default(),
1892            state: if terminal {
1893                ExecutionState::Completed
1894            } else {
1895                ExecutionState::Queued
1896            },
1897            outcome: terminal.then_some(runtime_api_contract::ExecutionOutcome {
1898                answer: "complete".into(),
1899                model_turns: 1,
1900                tool_calls: 0,
1901                input_tokens: Some(10),
1902                output_tokens: Some(2),
1903            }),
1904            failure: None,
1905            created_at_ms: 1,
1906            updated_at_ms: 1,
1907        }
1908    }
1909
1910    #[async_trait]
1911    impl RuntimeLink for FakeLink {
1912        async fn create_execution(
1913            &self,
1914            _: &RequestAuthority,
1915            _: &str,
1916            request: CreateExecutionRequest,
1917        ) -> Result<CreateExecutionResponse, LinkError> {
1918            let mut execution = view(self.terminal);
1919            execution.workspace_id = request.workspace_id;
1920            execution.model = request.model;
1921            execution.metadata = request.metadata;
1922            Ok(CreateExecutionResponse {
1923                execution,
1924                replayed: false,
1925            })
1926        }
1927        async fn execution(
1928            &self,
1929            _: &RequestAuthority,
1930            _: &ExecutionId,
1931        ) -> Result<ExecutionView, LinkError> {
1932            Ok(view(self.terminal))
1933        }
1934        async fn events(
1935            &self,
1936            _: &RequestAuthority,
1937            _: &ExecutionId,
1938            _: Option<u64>,
1939            _: usize,
1940        ) -> Result<EventPage, LinkError> {
1941            Ok(EventPage {
1942                items: Vec::new(),
1943                next_after: None,
1944            })
1945        }
1946        async fn submit_input(
1947            &self,
1948            _: &RequestAuthority,
1949            _: &ExecutionId,
1950            _: SubmitInputRequest,
1951        ) -> Result<ExecutionView, LinkError> {
1952            Ok(view(self.terminal))
1953        }
1954        async fn cancel(
1955            &self,
1956            _: &RequestAuthority,
1957            _: &ExecutionId,
1958        ) -> Result<ExecutionView, LinkError> {
1959            Ok(view(self.terminal))
1960        }
1961    }
1962
1963    fn app() -> Router {
1964        app_with_terminal(false)
1965    }
1966
1967    fn app_with_terminal(terminal: bool) -> Router {
1968        let auth = StaticTokenAuthenticator::new(
1969            "token",
1970            CallerScope {
1971                subject: "user".into(),
1972                tenant_id: "tenant".into(),
1973                project_id: "project".into(),
1974                capabilities: Vec::new(),
1975            },
1976        )
1977        .unwrap();
1978        router(
1979            Arc::new(FakeLink { terminal }),
1980            Arc::new(auth),
1981            Arc::new(AlwaysReady),
1982            RuntimeTransportConfig {
1983                agent_tasks: true,
1984                openai_responses: true,
1985                claude_messages: true,
1986                sse: true,
1987                websocket: true,
1988                websocket_max_message_bytes: 256 * 1024,
1989            },
1990        )
1991    }
1992
1993    fn create_request(authenticated: bool) -> Request<Body> {
1994        let mut request =
1995            Request::post("/v1/agent/tasks").header("content-type", "application/json");
1996        if authenticated {
1997            request = request.header("authorization", "Bearer token");
1998        }
1999        request.body(Body::from(r#"{"runtimeId":"runtime-1","input":{"text":"hello"},"limits":{"deadlineSeconds":60,"maxModelTurns":4,"maxToolCalls":8}}"#)).unwrap()
2000    }
2001
2002    #[tokio::test]
2003    async fn create_requires_authentication_and_returns_contract_body() {
2004        let unauthorized = app().oneshot(create_request(false)).await.unwrap();
2005        assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
2006
2007        let response = app().oneshot(create_request(true)).await.unwrap();
2008        assert_eq!(response.status(), StatusCode::ACCEPTED);
2009        assert!(response.headers().contains_key("x-request-id"));
2010        let body = response.into_body().collect().await.unwrap().to_bytes();
2011        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2012        assert_eq!(body["id"], "execution-1");
2013    }
2014
2015    #[tokio::test]
2016    async fn native_task_creation_generates_idempotency_and_returns_task_links() {
2017        let response = app()
2018            .oneshot(
2019                Request::post("/v1/agent/tasks")
2020                    .header("authorization", "Bearer token")
2021                    .header("content-type", "application/json")
2022                    .body(Body::from(
2023                        r#"{"runtimeId":"runtime-1","input":{"text":"hello"},"workspace":{"id":"workspace-2"}}"#,
2024                    ))
2025                    .unwrap(),
2026            )
2027            .await
2028            .unwrap();
2029        assert_eq!(response.status(), StatusCode::ACCEPTED);
2030        assert_eq!(
2031            response.headers().get(header::LOCATION).unwrap(),
2032            "/v1/agent/tasks/execution-1"
2033        );
2034        assert_eq!(response.headers().get(header::RETRY_AFTER).unwrap(), "1");
2035        let body = response.into_body().collect().await.unwrap().to_bytes();
2036        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2037        assert_eq!(body["object"], "agent.task");
2038        assert_eq!(body["workspace"]["id"], "workspace-2");
2039        assert!(body["metadata"]["requestId"].is_string());
2040        assert_eq!(
2041            body["links"]["stream"],
2042            "/v1/agent/tasks/execution-1/stream"
2043        );
2044    }
2045
2046    #[tokio::test]
2047    async fn openai_background_response_uses_openai_envelope() {
2048        let response = app()
2049            .oneshot(
2050                Request::post("/v1/responses")
2051                    .header("authorization", "Bearer token")
2052                    .header("content-type", "application/json")
2053                    .body(Body::from(
2054                        r#"{"model":"runtime-1","input":"hello","background":true,"max_output_tokens":256}"#,
2055                    ))
2056                    .unwrap(),
2057            )
2058            .await
2059            .unwrap();
2060        assert_eq!(response.status(), StatusCode::OK);
2061        let body = response.into_body().collect().await.unwrap().to_bytes();
2062        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2063        assert_eq!(body["object"], "response");
2064        assert!(body["id"].as_str().unwrap().starts_with("resp_"));
2065        assert_eq!(body["status"], "in_progress");
2066        assert_eq!(body["max_output_tokens"], 256);
2067    }
2068
2069    #[test]
2070    fn provider_parameters_fuse_into_one_generation_contract() {
2071        let openai: OpenAiResponseRequest = serde_json::from_value(json!({
2072            "model":"runtime-1",
2073            "input":"hello",
2074            "max_output_tokens":512,
2075            "temperature":0.25,
2076            "text":{"format":{"type":"json_object"}},
2077            "tool_choice":"none"
2078        }))
2079        .unwrap();
2080        let execution = openai_execution_request(&openai).unwrap();
2081        assert_eq!(execution.generation.max_output_tokens, Some(512));
2082        assert_eq!(execution.generation.temperature, Some(0.25));
2083        assert_eq!(
2084            execution.generation.response_format,
2085            Some(json!({"type":"json_object"}))
2086        );
2087        assert_eq!(execution.generation.tool_choice, ModelToolChoice::None);
2088
2089        let claude: ClaudeMessageRequest = serde_json::from_value(json!({
2090            "model":"runtime-1",
2091            "max_tokens":256,
2092            "messages":[{"role":"user","content":"hello"}],
2093            "temperature":0.4,
2094            "stop_sequences":["</done>"],
2095            "tool_choice":{"type":"none"}
2096        }))
2097        .unwrap();
2098        let execution = claude_execution_request(&claude).unwrap();
2099        assert_eq!(execution.generation.max_output_tokens, Some(256));
2100        assert_eq!(execution.generation.temperature, Some(0.4));
2101        assert_eq!(execution.generation.stop_sequences, ["</done>"]);
2102        assert_eq!(execution.generation.tool_choice, ModelToolChoice::None);
2103        assert_eq!(execution.generation.response_format, None);
2104    }
2105
2106    #[tokio::test]
2107    async fn provider_adapters_reject_fields_the_kernel_cannot_preserve() {
2108        let openai = app()
2109            .oneshot(
2110                Request::post("/v1/responses")
2111                    .header("authorization", "Bearer token")
2112                    .header("content-type", "application/json")
2113                    .body(Body::from(
2114                        r#"{"model":"runtime-1","input":"hello","tools":[{"type":"function","name":"remote"}]}"#,
2115                    ))
2116                    .unwrap(),
2117            )
2118            .await
2119            .unwrap();
2120        assert_eq!(openai.status(), StatusCode::BAD_REQUEST);
2121        let body = openai.into_body().collect().await.unwrap().to_bytes();
2122        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2123        assert_eq!(body["error"]["param"], "tools");
2124
2125        let claude = app()
2126            .oneshot(
2127                Request::post("/v1/messages")
2128                    .header("authorization", "Bearer token")
2129                    .header("content-type", "application/json")
2130                    .body(Body::from(
2131                        r#"{"model":"runtime-1","max_tokens":128,"messages":[{"role":"user","content":"hello"}]}"#,
2132                    ))
2133                    .unwrap(),
2134            )
2135            .await
2136            .unwrap();
2137        assert_eq!(claude.status(), StatusCode::BAD_REQUEST);
2138        let body = claude.into_body().collect().await.unwrap().to_bytes();
2139        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2140        assert_eq!(body["type"], "error");
2141        assert_eq!(body["error"]["type"], "invalid_request_error");
2142    }
2143
2144    #[tokio::test]
2145    async fn claude_sdk_style_api_key_authentication_is_accepted() {
2146        let response = app_with_terminal(true)
2147            .oneshot(
2148                Request::post("/v1/messages")
2149                    .header("x-api-key", "token")
2150                    .header("anthropic-version", "2023-06-01")
2151                    .header("content-type", "application/json")
2152                    .body(Body::from(
2153                        r#"{"model":"runtime-1","max_tokens":128,"messages":[{"role":"user","content":"hello"}]}"#,
2154                    ))
2155                    .unwrap(),
2156            )
2157            .await
2158            .unwrap();
2159        assert_eq!(response.status(), StatusCode::OK);
2160        let body = response.into_body().collect().await.unwrap().to_bytes();
2161        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2162        assert_eq!(body["type"], "message");
2163    }
2164
2165    #[tokio::test]
2166    async fn provider_sse_uses_native_event_names_and_terminal_order() {
2167        let openai = app_with_terminal(true)
2168            .oneshot(
2169                Request::post("/v1/responses")
2170                    .header("authorization", "Bearer token")
2171                    .header("content-type", "application/json")
2172                    .body(Body::from(
2173                        r#"{"model":"runtime-1","input":"complete","stream":true}"#,
2174                    ))
2175                    .unwrap(),
2176            )
2177            .await
2178            .unwrap();
2179        assert_eq!(openai.status(), StatusCode::OK);
2180        let openai = String::from_utf8(
2181            openai
2182                .into_body()
2183                .collect()
2184                .await
2185                .unwrap()
2186                .to_bytes()
2187                .to_vec(),
2188        )
2189        .unwrap();
2190        let created = openai.find("event: response.created").unwrap();
2191        let delta = openai.find("event: response.output_text.delta").unwrap();
2192        let completed = openai.find("event: response.completed").unwrap();
2193        assert!(created < delta && delta < completed);
2194
2195        let claude = app_with_terminal(true)
2196            .oneshot(
2197                Request::post("/v1/messages")
2198                    .header("authorization", "Bearer token")
2199                    .header("anthropic-version", "2023-06-01")
2200                    .header("content-type", "application/json")
2201                    .body(Body::from(
2202                        r#"{"model":"runtime-1","max_tokens":128,"messages":[{"role":"user","content":"complete"}],"stream":true}"#,
2203                    ))
2204                    .unwrap(),
2205            )
2206            .await
2207            .unwrap();
2208        assert_eq!(claude.status(), StatusCode::OK);
2209        let claude = String::from_utf8(
2210            claude
2211                .into_body()
2212                .collect()
2213                .await
2214                .unwrap()
2215                .to_bytes()
2216                .to_vec(),
2217        )
2218        .unwrap();
2219        let start = claude.find("event: message_start").unwrap();
2220        let delta = claude.find("event: content_block_delta").unwrap();
2221        let stop = claude.find("event: message_stop").unwrap();
2222        assert!(start < delta && delta < stop);
2223    }
2224
2225    #[test]
2226    fn sse_resume_accepts_standard_header_and_rejects_ambiguous_cursors() {
2227        let mut headers = HeaderMap::new();
2228        headers.insert("last-event-id", "17".parse().unwrap());
2229        assert_eq!(sse_resume_after(&headers, None).unwrap(), Some(17));
2230        assert_eq!(sse_resume_after(&headers, Some(17)).unwrap(), Some(17));
2231        assert!(sse_resume_after(&headers, Some(18)).is_err());
2232    }
2233
2234    #[test]
2235    fn provider_message_history_preserves_roles_for_the_kernel() {
2236        let openai: OpenAiResponseRequest = serde_json::from_value(json!({
2237            "model": "runtime-1",
2238            "input": [
2239                {"role":"user","content":"question"},
2240                {"role":"assistant","content":"draft"},
2241                {"role":"user","content":"continue"}
2242            ]
2243        }))
2244        .unwrap();
2245        let execution = openai_execution_request(&openai).unwrap();
2246        let RuntimeInput::Messages { messages } = execution.input else {
2247            panic!("provider history must remain structured");
2248        };
2249        assert_eq!(
2250            messages
2251                .iter()
2252                .map(|message| message.role)
2253                .collect::<Vec<_>>(),
2254            vec![
2255                RuntimeMessageRole::User,
2256                RuntimeMessageRole::Assistant,
2257                RuntimeMessageRole::User,
2258            ]
2259        );
2260    }
2261
2262    #[tokio::test]
2263    async fn trusted_gateway_scope_requires_the_shared_ingress_secret() {
2264        let authenticator =
2265            TrustedGatewayAuthenticator::new("a-runtime-gateway-secret-with-at-least-32-bytes")
2266                .unwrap();
2267        let mut headers = HeaderMap::new();
2268        headers.insert("authorization", "Bearer delegated-token".parse().unwrap());
2269        headers.insert("x-runtime-subject", "user".parse().unwrap());
2270        headers.insert("x-runtime-tenant-id", "tenant".parse().unwrap());
2271        headers.insert("x-runtime-project-id", "project".parse().unwrap());
2272        assert!(authenticator.authenticate(&headers).await.is_err());
2273        headers.insert(
2274            "x-runtime-gateway-secret",
2275            "a-runtime-gateway-secret-with-at-least-32-bytes"
2276                .parse()
2277                .unwrap(),
2278        );
2279        let authority = authenticator.authenticate(&headers).await.unwrap();
2280        assert_eq!(authority.caller.subject, "user");
2281    }
2282
2283    #[tokio::test]
2284    async fn transport_routes_are_registered_only_when_enabled() {
2285        let auth = StaticTokenAuthenticator::new(
2286            "token",
2287            CallerScope {
2288                subject: "user".into(),
2289                tenant_id: "tenant".into(),
2290                project_id: "project".into(),
2291                capabilities: Vec::new(),
2292            },
2293        )
2294        .unwrap();
2295        let app = router(
2296            Arc::new(FakeLink { terminal: false }),
2297            Arc::new(auth),
2298            Arc::new(AlwaysReady),
2299            RuntimeTransportConfig {
2300                agent_tasks: true,
2301                openai_responses: false,
2302                claude_messages: false,
2303                sse: false,
2304                websocket: false,
2305                websocket_max_message_bytes: 1024,
2306            },
2307        );
2308        let stream = app
2309            .clone()
2310            .oneshot(
2311                Request::get("/v1/agent/tasks/execution-1/stream")
2312                    .header("authorization", "Bearer token")
2313                    .body(Body::empty())
2314                    .unwrap(),
2315            )
2316            .await
2317            .unwrap();
2318        assert_eq!(stream.status(), StatusCode::NOT_FOUND);
2319        let openai = app
2320            .clone()
2321            .oneshot(Request::post("/v1/responses").body(Body::empty()).unwrap())
2322            .await
2323            .unwrap();
2324        assert_eq!(openai.status(), StatusCode::NOT_FOUND);
2325        let openapi = app
2326            .oneshot(Request::get("/openapi.json").body(Body::empty()).unwrap())
2327            .await
2328            .unwrap();
2329        let body = openapi.into_body().collect().await.unwrap().to_bytes();
2330        let document: serde_json::Value = serde_json::from_slice(&body).unwrap();
2331        assert!(
2332            document["paths"]
2333                .get("/v1/agent/tasks/{taskId}/stream")
2334                .is_none()
2335        );
2336        assert!(
2337            document["paths"]
2338                .get("/v1/agent/tasks/{taskId}/ws")
2339                .is_none()
2340        );
2341        assert!(document["paths"].get("/v1/responses").is_none());
2342        assert!(document["paths"].get("/v1/messages").is_none());
2343    }
2344
2345    #[tokio::test]
2346    async fn websocket_is_authenticated_and_accepts_scoped_commands() {
2347        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2348        let address = listener.local_addr().unwrap();
2349        let server = tokio::spawn(async move { axum::serve(listener, app()).await.unwrap() });
2350
2351        let mut request = format!("ws://{address}/v1/agent/tasks/execution-1/ws")
2352            .into_client_request()
2353            .unwrap();
2354        request
2355            .headers_mut()
2356            .insert("authorization", "Bearer token".parse().unwrap());
2357        let (mut socket, response) = connect_async(request).await.unwrap();
2358        assert_eq!(response.status(), 101);
2359
2360        let connected = socket.next().await.unwrap().unwrap().into_text().unwrap();
2361        let connected: AgentTaskWebSocketServerMessage = serde_json::from_str(&connected).unwrap();
2362        assert!(matches!(
2363            connected,
2364            AgentTaskWebSocketServerMessage::Connected { .. }
2365        ));
2366
2367        socket
2368            .send(ClientMessage::Text(r#"{"type":"cancel"}"#.into()))
2369            .await
2370            .unwrap();
2371        let execution = socket.next().await.unwrap().unwrap().into_text().unwrap();
2372        let execution: AgentTaskWebSocketServerMessage = serde_json::from_str(&execution).unwrap();
2373        assert!(matches!(
2374            execution,
2375            AgentTaskWebSocketServerMessage::Task { .. }
2376        ));
2377        socket.close(None).await.unwrap();
2378        server.abort();
2379    }
2380}