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 close(&mut self) -> Result<()> {
177 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}