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 acquire_control(&mut self) -> Result<crate::RuntimeLeaseSnapshot> {
177        self.remote.acquire_control().await.map_err(frontend_error)
178    }
179
180    async fn heartbeat(&mut self) -> Result<crate::RuntimeLeaseSnapshot> {
181        self.remote.heartbeat().await.map_err(frontend_error)
182    }
183
184    async fn detach(&mut self) -> Result<crate::RuntimeLeaseSnapshot> {
185        self.remote.detach().await.map_err(frontend_error)
186    }
187
188    async fn close(&mut self) -> Result<()> {
189        // Dropping this HTTP/SSE attachment is a detach. The terminal-owned
190        // SDK runtime keeps running until its own frontend exits or closes it.
191        if !self.remote.is_disconnected() {
192            self.remote.detach().await.map_err(frontend_error)?;
193        }
194        Ok(())
195    }
196}
197
198impl SupercodeHttpRuntimeConnection {
199    fn project_live_event(
200        &mut self,
201        sequence: u64,
202        kind: String,
203        payload: Value,
204    ) -> Option<HarnessEvent> {
205        if kind == "request" {
206            self.record_request(&payload);
207        }
208        Some(HarnessEvent {
209            sequence: Some(sequence),
210            kind,
211            payload,
212        })
213    }
214
215    fn record_request(&mut self, payload: &Value) {
216        let Some(request) = payload.get("request") else {
217            return;
218        };
219        let Some(id) = request.get("id").and_then(Value::as_u64) else {
220            return;
221        };
222        let Some(kind) = request
223            .get("kind")
224            .cloned()
225            .and_then(|value| serde_json::from_value::<FrontendRequestKind>(value).ok())
226        else {
227            return;
228        };
229        self.requests.insert(id, kind);
230    }
231}
232
233fn frontend_response(
234    request_id: u64,
235    kind: FrontendRequestKind,
236    response: Value,
237) -> Result<FrontendResponse> {
238    match kind {
239        FrontendRequestKind::Approval => {
240            let decision = match response.get("decision").and_then(Value::as_str) {
241                Some("allow") => FrontendApprovalDecision::Allow,
242                Some("allow_for_session") => FrontendApprovalDecision::AllowForSession,
243                Some("deny") => FrontendApprovalDecision::Deny,
244                other => {
245                    return Err(crate::Error::Other(format!(
246                        "invalid Supercode approval response: {other:?}"
247                    )))
248                }
249            };
250            Ok(FrontendResponse::Approval {
251                request_id,
252                decision,
253            })
254        }
255        FrontendRequestKind::Elicitation | FrontendRequestKind::Other => {
256            let action = match response.get("action").and_then(Value::as_str) {
257                Some("accept") => FrontendElicitationAction::Accept,
258                Some("decline") => FrontendElicitationAction::Decline,
259                Some("cancel") => FrontendElicitationAction::Cancel,
260                other => {
261                    return Err(crate::Error::Other(format!(
262                        "invalid Supercode elicitation response: {other:?}"
263                    )))
264                }
265            };
266            let content = response.get("content").cloned();
267            Ok(if kind == FrontendRequestKind::Elicitation {
268                FrontendResponse::Elicitation {
269                    request_id,
270                    action,
271                    content,
272                }
273            } else {
274                FrontendResponse::Other {
275                    request_id,
276                    action,
277                    content,
278                }
279            })
280        }
281    }
282}
283
284fn frontend_error(error: FrontendRuntimeError) -> crate::Error {
285    error.into()
286}
287
288fn normalized_path(path: &std::path::Path) -> std::path::PathBuf {
289    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
290}