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