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