supercode_harness/runtime/
supercode_http.rs1use 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
20pub struct SupercodeHttpRuntimeBackend {
23 receipt: ResolvedLiveRuntime,
24}
25
26impl SupercodeHttpRuntimeBackend {
27 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 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}