1use std::path::PathBuf;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6pub type RemoteRunnerProviderId = String;
7pub type RemoteRunnerSessionId = String;
8pub type RunnerDestinationId = String;
9pub type RunnerCommandId = String;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct RunnerCapabilities {
13 pub command_exec: bool,
14 pub file_read: bool,
15 pub file_write: bool,
16 pub port_preview: bool,
17 pub snapshots: bool,
18 pub cancellation: bool,
19 #[serde(default)]
20 pub artifact_export: bool,
21 #[serde(default)]
22 pub mounts: RunnerMountCapabilities,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
26pub struct RunnerMountCapabilities {
27 #[serde(default)]
28 pub s3: bool,
29 #[serde(default)]
30 pub gcs: bool,
31 #[serde(default)]
32 pub r2: bool,
33 #[serde(default)]
34 pub azure_blob: bool,
35 #[serde(default)]
36 pub box_storage: bool,
37 #[serde(default)]
38 pub provider_native: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct RunnerDestination {
43 pub id: RunnerDestinationId,
44 pub provider_id: RemoteRunnerProviderId,
45 #[serde(default)]
46 pub config: serde_json::Value,
47 #[serde(default)]
48 pub default_manifest: RunnerManifest,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
52pub struct RunnerManifest {
53 #[serde(default)]
54 pub entries: Vec<RunnerManifestEntry>,
55 #[serde(default)]
56 pub mounts: Vec<RunnerMount>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct RunnerManifestEntry {
61 pub source: PathBuf,
62 pub target: PathBuf,
63 pub writable: bool,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct RunnerMount {
68 pub name: String,
69 pub path: PathBuf,
70 pub read_only: bool,
71 #[serde(default)]
72 pub intent: RunnerMountIntent,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct RunnerMountIntent {
77 pub kind: RunnerMountKind,
78 pub uri: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub credentials: Option<RunnerSecretRef>,
81}
82
83impl Default for RunnerMountIntent {
84 fn default() -> Self {
85 Self {
86 kind: RunnerMountKind::ProviderNative,
87 uri: String::new(),
88 credentials: None,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[serde(rename_all = "snake_case")]
95pub enum RunnerMountKind {
96 S3,
97 Gcs,
98 R2,
99 AzureBlob,
100 BoxStorage,
101 ProviderNative,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105pub struct RunnerSecretRef {
106 pub id: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub struct RunnerSnapshotRef {
111 pub provider_id: RemoteRunnerProviderId,
112 pub snapshot_id: String,
113 #[serde(default)]
114 pub metadata: serde_json::Value,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub struct RunnerSessionState {
119 pub provider_id: RemoteRunnerProviderId,
120 pub session_id: RemoteRunnerSessionId,
121 pub destination_id: RunnerDestinationId,
122 #[serde(default)]
123 pub snapshot: Option<RunnerSnapshotRef>,
124 #[serde(default)]
125 pub metadata: serde_json::Value,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub struct ThreadRunnerBinding {
136 pub destination: RunnerDestination,
137 pub workspace: PathBuf,
139 #[serde(default)]
146 pub read_roots: Vec<PathBuf>,
147}
148
149#[derive(Clone)]
156pub struct RemoteWorkspace {
157 pub session: Arc<dyn RemoteRunnerSession>,
158 pub root: PathBuf,
159 pub read_roots: Vec<PathBuf>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167pub struct RunnerCommandRequest {
168 pub command_id: RunnerCommandId,
169 pub program: String,
170 #[serde(default)]
171 pub args: Vec<String>,
172 #[serde(default)]
173 pub cwd: Option<PathBuf>,
174 #[serde(default)]
175 pub env: Vec<(String, String)>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
179pub struct RunnerCommandResult {
180 pub command_id: RunnerCommandId,
181 pub exit_code: Option<i32>,
182 pub stdout: String,
183 pub stderr: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
187pub struct RunnerFileReadRequest {
188 pub path: PathBuf,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct RunnerFileReadResult {
193 pub path: PathBuf,
194 pub contents: Vec<u8>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198pub struct RunnerFileWriteRequest {
199 pub path: PathBuf,
200 pub contents: Vec<u8>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204pub struct RunnerPortRequest {
205 pub port: u16,
206 pub label: Option<String>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
210pub struct RunnerPortResult {
211 pub port: u16,
212 pub url: Option<String>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
216pub struct RunnerArtifactExportRequest {
217 pub path: PathBuf,
218 #[serde(default)]
219 pub recursive: bool,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub struct RunnerArtifactExportResult {
224 pub path: PathBuf,
225 pub artifact_id: String,
226 pub url: Option<String>,
227 #[serde(default)]
228 pub metadata: serde_json::Value,
229}
230
231#[async_trait::async_trait]
232pub trait RemoteRunnerProvider: Send + Sync + 'static {
233 fn id(&self) -> RemoteRunnerProviderId;
234 fn capabilities(&self) -> RunnerCapabilities;
235
236 fn setup_hint(&self) -> Option<String> {
243 None
244 }
245
246 async fn create_session(
247 &self,
248 destination: RunnerDestination,
249 ) -> anyhow::Result<Arc<dyn RemoteRunnerSession>>;
250
251 async fn validate_destination(&self, _destination: &RunnerDestination) -> anyhow::Result<()> {
252 Ok(())
253 }
254
255 async fn resume_session(
256 &self,
257 state: RunnerSessionState,
258 ) -> anyhow::Result<Arc<dyn RemoteRunnerSession>>;
259}
260
261#[async_trait::async_trait]
262pub trait RemoteRunnerSession: Send + Sync + 'static {
263 fn state(&self) -> RunnerSessionState;
264
265 async fn run_command(
266 &self,
267 request: RunnerCommandRequest,
268 ) -> anyhow::Result<RunnerCommandResult>;
269
270 async fn cancel_command(&self, _command_id: &RunnerCommandId) -> anyhow::Result<bool> {
271 Ok(false)
272 }
273
274 async fn read_file(
275 &self,
276 request: RunnerFileReadRequest,
277 ) -> anyhow::Result<RunnerFileReadResult>;
278
279 async fn write_file(&self, request: RunnerFileWriteRequest) -> anyhow::Result<()>;
280
281 async fn expose_port(&self, request: RunnerPortRequest) -> anyhow::Result<RunnerPortResult>;
282
283 async fn export_artifact(
284 &self,
285 _request: RunnerArtifactExportRequest,
286 ) -> anyhow::Result<RunnerArtifactExportResult> {
287 anyhow::bail!("runner artifact export is not supported by this provider")
288 }
289
290 async fn snapshot(&self) -> anyhow::Result<Option<RunnerSnapshotRef>>;
291
292 async fn close(&self) -> anyhow::Result<()>;
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn remote_runner_types_round_trip_json() {
301 let destination = RunnerDestination {
302 id: "local".to_string(),
303 provider_id: "unix-local".to_string(),
304 config: serde_json::json!({ "root": "." }),
305 default_manifest: RunnerManifest {
306 entries: vec![RunnerManifestEntry {
307 source: "src".into(),
308 target: "workspace/src".into(),
309 writable: true,
310 }],
311 mounts: vec![RunnerMount {
312 name: "cache".to_string(),
313 path: ".cache".into(),
314 read_only: false,
315 intent: RunnerMountIntent::default(),
316 }],
317 },
318 };
319
320 let encoded = serde_json::to_value(&destination).unwrap();
321 let decoded: RunnerDestination = serde_json::from_value(encoded).unwrap();
322
323 assert_eq!(decoded, destination);
324 }
325
326 #[test]
327 fn command_and_port_operations_are_protocol_safe() {
328 let command = RunnerCommandRequest {
329 command_id: "cmd-1".to_string(),
330 program: "sh".to_string(),
331 args: vec!["-lc".to_string(), "echo hi".to_string()],
332 cwd: Some("workspace".into()),
333 env: vec![("RUST_LOG".to_string(), "info".to_string())],
334 };
335 let port = RunnerPortResult {
336 port: 3000,
337 url: Some("https://preview.example".to_string()),
338 };
339
340 assert_eq!(
341 serde_json::from_value::<RunnerCommandRequest>(serde_json::to_value(&command).unwrap())
342 .unwrap(),
343 command
344 );
345 assert_eq!(
346 serde_json::from_value::<RunnerPortResult>(serde_json::to_value(&port).unwrap())
347 .unwrap(),
348 port
349 );
350 }
351
352 #[test]
353 fn mount_and_artifact_operations_are_protocol_safe() {
354 let mount = RunnerMount {
355 name: "dataset".to_string(),
356 path: "mnt/dataset".into(),
357 read_only: true,
358 intent: RunnerMountIntent {
359 kind: RunnerMountKind::R2,
360 uri: "r2://bucket/prefix".to_string(),
361 credentials: Some(RunnerSecretRef {
362 id: "r2-readonly".to_string(),
363 }),
364 },
365 };
366 let artifact = RunnerArtifactExportResult {
367 path: "out/report.json".into(),
368 artifact_id: "artifact-1".to_string(),
369 url: Some("https://artifacts.example/report.json".to_string()),
370 metadata: serde_json::json!({ "size": 128 }),
371 };
372
373 assert_eq!(
374 serde_json::from_value::<RunnerMount>(serde_json::to_value(&mount).unwrap()).unwrap(),
375 mount
376 );
377 assert_eq!(
378 serde_json::from_value::<RunnerArtifactExportResult>(
379 serde_json::to_value(&artifact).unwrap()
380 )
381 .unwrap(),
382 artifact
383 );
384 }
385}