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