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