Skip to main content

supercode_harness/runtime/
supercode_http.rs

1//! Runtime adapter for a trusted, already-hosted Supercode SDK runtime.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use serde_json::Value;
8
9use super::{
10    HarnessEvent, RuntimeAttachRequest, RuntimeBackend, RuntimeCapabilities, RuntimeConnection,
11    RuntimeEndpoint, RuntimeHandle, RuntimeInput, RuntimeStartRequest,
12};
13use crate::frontend::HttpFrontendRuntime;
14use crate::{
15    FrontendApprovalDecision, FrontendAttachment, FrontendElicitationAction, FrontendRequestKind,
16    FrontendResponse, FrontendRuntime, FrontendRuntimeError, HarnessId, ResolvedLiveRuntime,
17    Result,
18};
19
20/// Adapter that joins one authenticated Supercode SDK runtime without taking
21/// ownership of the process or creating a second model loop.
22pub struct SupercodeHttpRuntimeBackend {
23    receipt: ResolvedLiveRuntime,
24}
25
26impl SupercodeHttpRuntimeBackend {
27    /// Build an adapter from a trusted local receipt resolved by the host.
28    pub fn new(receipt: ResolvedLiveRuntime) -> Self {
29        Self { receipt }
30    }
31}
32
33#[async_trait]
34impl RuntimeBackend for SupercodeHttpRuntimeBackend {
35    fn harness(&self) -> HarnessId {
36        HarnessId::new(&self.receipt.source.harness)
37    }
38
39    fn capabilities(&self) -> RuntimeCapabilities {
40        RuntimeCapabilities {
41            start_session: false,
42            resume_session: false,
43            attach_existing_process: true,
44            send_input: true,
45            stream_events: true,
46            interrupt: true,
47            respond_to_requests: true,
48        }
49    }
50
51    async fn start(&self, _request: RuntimeStartRequest) -> Result<Box<dyn RuntimeConnection>> {
52        Err(crate::Error::Other(
53            "a Supercode live receipt can only attach to its existing runtime".into(),
54        ))
55    }
56
57    async fn attach(&self, request: RuntimeAttachRequest) -> Result<Box<dyn RuntimeConnection>> {
58        self.attach_existing(request).await
59    }
60
61    async fn attach_existing(
62        &self,
63        request: RuntimeAttachRequest,
64    ) -> Result<Box<dyn RuntimeConnection>> {
65        if request.runtime_id != self.receipt.source.session_id {
66            return Err(crate::Error::Other(
67                "live runtime receipt does not belong to the requested source session".into(),
68            ));
69        }
70        if request.cwd.as_ref().is_some_and(|cwd| {
71            normalized_path(cwd) != normalized_path(&self.receipt.source.workspace)
72        }) {
73            return Err(crate::Error::Other(
74                "live runtime receipt does not belong to the requested workspace".into(),
75            ));
76        }
77        let remote =
78            HttpFrontendRuntime::connect(self.receipt.base_url.clone(), self.receipt.token.clone())
79                .await
80                .map_err(frontend_error)?;
81        let descriptor = FrontendRuntime::describe(remote.as_ref())
82            .await
83            .map_err(frontend_error)?;
84        if descriptor.session_id != self.receipt.runtime_session_id
85            || descriptor.source_harness.as_deref() != Some(self.receipt.source.harness.as_str())
86        {
87            return Err(crate::Error::Other(
88                "live runtime identity did not match its trusted receipt".into(),
89            ));
90        }
91        let attachment = FrontendRuntime::attach(remote.as_ref(), 10_000)
92            .await
93            .map_err(frontend_error)?;
94        Ok(Box::new(SupercodeHttpRuntimeConnection {
95            handle: RuntimeHandle {
96                harness: HarnessId::new(&self.receipt.source.harness),
97                runtime_id: descriptor.session_id,
98                endpoint: RuntimeEndpoint::Http {
99                    base_url: self.receipt.endpoint.to_string(),
100                    protocol: "supercode-frontend-http-v1".into(),
101                },
102            },
103            remote,
104            attachment,
105            requests: BTreeMap::new(),
106        }))
107    }
108}
109
110struct SupercodeHttpRuntimeConnection {
111    handle: RuntimeHandle,
112    remote: Arc<HttpFrontendRuntime>,
113    attachment: FrontendAttachment,
114    requests: BTreeMap<u64, FrontendRequestKind>,
115}
116
117#[async_trait]
118impl RuntimeConnection for SupercodeHttpRuntimeConnection {
119    fn handle(&self) -> &RuntimeHandle {
120        &self.handle
121    }
122
123    async fn send_input(&mut self, input: RuntimeInput) -> Result<Option<String>> {
124        FrontendRuntime::send_input_with_images(self.remote.clone(), input.text, input.image_urls)
125            .await
126            .map_err(frontend_error)?;
127        Ok(None)
128    }
129
130    async fn next_event(&mut self) -> Result<Option<HarnessEvent>> {
131        loop {
132            let event = self.attachment.next_event().await.map_err(frontend_error)?;
133            if let Some(event) = self.project_live_event(event.sequence, event.kind, event.payload)
134            {
135                return Ok(Some(event));
136            }
137        }
138    }
139
140    async fn interrupt(&mut self) -> Result<()> {
141        FrontendRuntime::interrupt(self.remote.as_ref())
142            .await
143            .map(|_| ())
144            .map_err(frontend_error)
145    }
146
147    async fn respond(&mut self, request_id: Value, response: Value) -> Result<()> {
148        let descriptor = FrontendRuntime::describe(self.remote.as_ref())
149            .await
150            .map_err(frontend_error)?;
151        if !descriptor.actions.respond {
152            return Err(crate::Error::Other(
153                "SDK action `respond` is not supported by this runtime".into(),
154            ));
155        }
156        let id = request_id
157            .as_u64()
158            .ok_or_else(|| crate::Error::Other("Supercode request id must be an integer".into()))?;
159        let kind = self
160            .requests
161            .remove(&id)
162            .ok_or_else(|| crate::Error::Other(format!("Supercode request {id} is not pending")))?;
163        let response = frontend_response(id, kind, response)?;
164        FrontendRuntime::respond(self.remote.as_ref(), response)
165            .await
166            .map_err(frontend_error)
167    }
168
169    async fn close(&mut self) -> Result<()> {
170        // Dropping this HTTP/SSE attachment is a detach. The terminal-owned
171        // SDK runtime keeps running until its own frontend exits or closes it.
172        Ok(())
173    }
174}
175
176impl SupercodeHttpRuntimeConnection {
177    fn project_live_event(
178        &mut self,
179        sequence: u64,
180        kind: String,
181        payload: Value,
182    ) -> Option<HarnessEvent> {
183        if kind == "request" {
184            self.record_request(&payload);
185        }
186        Some(HarnessEvent {
187            sequence: Some(sequence),
188            kind,
189            payload,
190        })
191    }
192
193    fn record_request(&mut self, payload: &Value) {
194        let Some(request) = payload.get("request") else {
195            return;
196        };
197        let Some(id) = request.get("id").and_then(Value::as_u64) else {
198            return;
199        };
200        let Some(kind) = request
201            .get("kind")
202            .cloned()
203            .and_then(|value| serde_json::from_value::<FrontendRequestKind>(value).ok())
204        else {
205            return;
206        };
207        self.requests.insert(id, kind);
208    }
209}
210
211fn frontend_response(
212    request_id: u64,
213    kind: FrontendRequestKind,
214    response: Value,
215) -> Result<FrontendResponse> {
216    match kind {
217        FrontendRequestKind::Approval => {
218            let decision = match response.get("decision").and_then(Value::as_str) {
219                Some("allow") => FrontendApprovalDecision::Allow,
220                Some("allow_for_session") => FrontendApprovalDecision::AllowForSession,
221                Some("deny") => FrontendApprovalDecision::Deny,
222                other => {
223                    return Err(crate::Error::Other(format!(
224                        "invalid Supercode approval response: {other:?}"
225                    )))
226                }
227            };
228            Ok(FrontendResponse::Approval {
229                request_id,
230                decision,
231            })
232        }
233        FrontendRequestKind::Elicitation | FrontendRequestKind::Other => {
234            let action = match response.get("action").and_then(Value::as_str) {
235                Some("accept") => FrontendElicitationAction::Accept,
236                Some("decline") => FrontendElicitationAction::Decline,
237                Some("cancel") => FrontendElicitationAction::Cancel,
238                other => {
239                    return Err(crate::Error::Other(format!(
240                        "invalid Supercode elicitation response: {other:?}"
241                    )))
242                }
243            };
244            let content = response.get("content").cloned();
245            Ok(if kind == FrontendRequestKind::Elicitation {
246                FrontendResponse::Elicitation {
247                    request_id,
248                    action,
249                    content,
250                }
251            } else {
252                FrontendResponse::Other {
253                    request_id,
254                    action,
255                    content,
256                }
257            })
258        }
259    }
260}
261
262fn frontend_error(error: FrontendRuntimeError) -> crate::Error {
263    error.into()
264}
265
266fn normalized_path(path: &std::path::Path) -> std::path::PathBuf {
267    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
268}