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 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 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}