1use std::sync::Arc;
2use std::time::SystemTime;
3
4use tonic::codegen::async_trait;
5use tonic::{Request as GrpcRequest, Response as GrpcResponse, Status};
6
7use crate::agent_provider::{AgentPreparedWorkspace, AgentWorkspace};
8use crate::api::RuntimeMetadata;
9use crate::error::Result as ProviderResult;
10use crate::generated::v1::{self as pb};
11use crate::protocol;
12use crate::rpc_status::rpc_status;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
15#[repr(i32)]
16pub enum RuntimeEgressMode {
18 #[default]
19 Unspecified = 0,
21 None = 1,
23 Cidr = 2,
25 Hostname = 3,
27}
28
29impl RuntimeEgressMode {
30 pub const fn as_i32(self) -> i32 {
32 self as i32
33 }
34
35 pub const fn from_i32_lossy(value: i32) -> Self {
38 match value {
39 1 => Self::None,
40 2 => Self::Cidr,
41 3 => Self::Hostname,
42 _ => Self::Unspecified,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
48pub struct RuntimeSupport {
50 pub can_host_apps: bool,
52 pub egress_mode: RuntimeEgressMode,
54 pub supports_prepare_workspace: bool,
56}
57
58#[derive(Clone, Debug, Default, PartialEq)]
59pub struct RuntimeSession {
61 pub id: String,
63 pub state: String,
65 pub metadata: std::collections::BTreeMap<String, String>,
67 pub lifecycle: Option<RuntimeSessionLifecycle>,
69 pub state_reason: String,
71 pub state_message: String,
73}
74
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct RuntimeSessionLifecycle {
78 pub started_at: Option<SystemTime>,
80 pub recommended_drain_at: Option<SystemTime>,
82 pub expires_at: Option<SystemTime>,
84}
85
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
87pub struct RuntimeImagePullAuth {
89 pub docker_config_json: String,
91}
92
93#[derive(Clone, Debug, Default, PartialEq)]
94pub struct StartRuntimeSessionRequest {
96 pub app_name: String,
98 pub template: String,
100 pub image: String,
102 pub metadata: std::collections::BTreeMap<String, String>,
104 pub image_pull_auth: Option<RuntimeImagePullAuth>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
109pub struct GetRuntimeSessionRequest {
111 pub session_id: String,
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
116pub struct ListRuntimeSessionsRequest {
118 pub page_size: i32,
120 pub page_token: String,
122}
123
124#[derive(Clone, Debug, Default, PartialEq)]
125pub struct ListRuntimeSessionsResponse {
127 pub sessions: Vec<RuntimeSession>,
129 pub next_page_token: String,
131}
132
133#[derive(Clone, Debug, Default, PartialEq, Eq)]
134pub struct StopRuntimeSessionRequest {
136 pub session_id: String,
138}
139
140#[derive(Clone, Debug, Default, PartialEq)]
141pub struct PrepareRuntimeWorkspaceRequest {
143 pub session_id: String,
145 pub agent_session_id: String,
147 pub workspace: Option<AgentWorkspace>,
149}
150
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152pub struct PrepareRuntimeWorkspaceResponse {
154 pub workspace: Option<AgentPreparedWorkspace>,
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Eq)]
159pub struct RemoveRuntimeWorkspaceRequest {
161 pub session_id: String,
163 pub agent_session_id: String,
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct StartHostedAppRequest {
170 pub session_id: String,
172 pub app_name: String,
174 pub command: String,
176 pub args: Vec<String>,
178 pub env: std::collections::BTreeMap<String, String>,
180 pub allowed_hosts: Vec<String>,
182 pub default_action: String,
184 pub host_binary: String,
186 pub workdir: String,
188}
189
190#[derive(Clone, Debug, Default, PartialEq, Eq)]
191pub struct HostedApp {
193 pub id: String,
195 pub session_id: String,
197 pub app_name: String,
199 pub dial_target: String,
201}
202
203fn support_to_proto(value: RuntimeSupport) -> pb::RuntimeSupport {
204 pb::RuntimeSupport {
205 can_host_apps: value.can_host_apps,
206 egress_mode: value.egress_mode.as_i32(),
207 supports_prepare_workspace: value.supports_prepare_workspace,
208 }
209}
210
211fn session_to_proto(value: RuntimeSession) -> pb::RuntimeSession {
212 pb::RuntimeSession {
213 id: value.id,
214 state: value.state,
215 metadata: value.metadata,
216 lifecycle: value
217 .lifecycle
218 .map(|lifecycle| pb::RuntimeSessionLifecycle {
219 started_at: lifecycle
220 .started_at
221 .map(protocol::timestamp_from_system_time),
222 recommended_drain_at: lifecycle
223 .recommended_drain_at
224 .map(protocol::timestamp_from_system_time),
225 expires_at: lifecycle
226 .expires_at
227 .map(protocol::timestamp_from_system_time),
228 }),
229 state_reason: value.state_reason,
230 state_message: value.state_message,
231 }
232}
233
234fn start_session_request_from_proto(
235 value: pb::StartRuntimeSessionRequest,
236) -> StartRuntimeSessionRequest {
237 StartRuntimeSessionRequest {
238 app_name: value.app_name,
239 template: value.template,
240 image: value.image,
241 metadata: value.metadata,
242 image_pull_auth: value.image_pull_auth.map(|auth| RuntimeImagePullAuth {
243 docker_config_json: auth.docker_config_json,
244 }),
245 }
246}
247
248fn list_sessions_response_to_proto(
249 value: ListRuntimeSessionsResponse,
250) -> pb::ListRuntimeSessionsResponse {
251 pb::ListRuntimeSessionsResponse {
252 sessions: value.sessions.into_iter().map(session_to_proto).collect(),
253 next_page_token: value.next_page_token,
254 }
255}
256
257fn list_sessions_request_from_proto(
258 value: pb::ListRuntimeSessionsRequest,
259) -> std::result::Result<ListRuntimeSessionsRequest, Status> {
260 let mut page_size = value.page_size;
261 if page_size < 0 {
262 return Err(Status::invalid_argument("page_size must be non-negative"));
263 }
264 if page_size == 0 {
265 page_size = 100;
266 }
267 if page_size > 200 {
268 page_size = 200;
269 }
270 Ok(ListRuntimeSessionsRequest {
271 page_size,
272 page_token: value.page_token,
273 })
274}
275
276fn prepare_workspace_request_from_proto(
277 value: pb::PrepareRuntimeWorkspaceRequest,
278) -> PrepareRuntimeWorkspaceRequest {
279 PrepareRuntimeWorkspaceRequest {
280 session_id: value.session_id,
281 agent_session_id: value.agent_session_id,
282 workspace: value.workspace.map(|workspace| AgentWorkspace {
283 checkouts: workspace
284 .checkouts
285 .into_iter()
286 .map(
287 |checkout| crate::agent_provider::AgentWorkspaceGitCheckout {
288 url: checkout.url,
289 reference: checkout.r#ref,
290 path: checkout.path,
291 },
292 )
293 .collect(),
294 cwd: workspace.cwd,
295 }),
296 }
297}
298
299fn prepare_workspace_response_to_proto(
300 value: PrepareRuntimeWorkspaceResponse,
301) -> pb::PrepareRuntimeWorkspaceResponse {
302 pb::PrepareRuntimeWorkspaceResponse {
303 workspace: value.workspace.map(|workspace| pb::PreparedAgentWorkspace {
304 root: workspace.root,
305 cwd: workspace.cwd,
306 }),
307 }
308}
309
310fn start_app_request_from_proto(value: pb::StartHostedAppRequest) -> StartHostedAppRequest {
311 StartHostedAppRequest {
312 session_id: value.session_id,
313 app_name: value.app_name,
314 command: value.command,
315 args: value.args,
316 env: value.env,
317 allowed_hosts: value.allowed_hosts,
318 default_action: value.default_action,
319 host_binary: value.host_binary,
320 workdir: value.workdir,
321 }
322}
323
324fn hosted_app_to_proto(value: HostedApp) -> pb::HostedApp {
325 pb::HostedApp {
326 id: value.id,
327 session_id: value.session_id,
328 app_name: value.app_name,
329 dial_target: value.dial_target,
330 }
331}
332
333#[async_trait]
334pub trait RuntimeProvider: Send + Sync + 'static {
336 async fn configure(
338 &self,
339 _name: &str,
340 _config: serde_json::Map<String, serde_json::Value>,
341 ) -> ProviderResult<()> {
342 Ok(())
343 }
344
345 fn metadata(&self) -> Option<RuntimeMetadata> {
347 None
348 }
349
350 fn warnings(&self) -> Vec<String> {
352 Vec::new()
353 }
354
355 async fn health_check(&self) -> ProviderResult<()> {
357 Ok(())
358 }
359
360 async fn start(&self) -> ProviderResult<()> {
362 Ok(())
363 }
364
365 async fn close(&self) -> ProviderResult<()> {
367 Ok(())
368 }
369
370 async fn get_support(&self, _request: ()) -> ProviderResult<RuntimeSupport> {
372 Err(crate::Error::unimplemented(
373 "runtime get support is not implemented",
374 ))
375 }
376
377 async fn start_session(
379 &self,
380 _request: StartRuntimeSessionRequest,
381 ) -> ProviderResult<RuntimeSession> {
382 Err(crate::Error::unimplemented(
383 "runtime start session is not implemented",
384 ))
385 }
386
387 async fn get_session(
389 &self,
390 _request: GetRuntimeSessionRequest,
391 ) -> ProviderResult<RuntimeSession> {
392 Err(crate::Error::unimplemented(
393 "runtime get session is not implemented",
394 ))
395 }
396
397 async fn list_sessions(
399 &self,
400 _request: ListRuntimeSessionsRequest,
401 ) -> ProviderResult<ListRuntimeSessionsResponse> {
402 Err(crate::Error::unimplemented(
403 "runtime list sessions is not implemented",
404 ))
405 }
406
407 async fn stop_session(&self, _request: StopRuntimeSessionRequest) -> ProviderResult<()> {
409 Err(crate::Error::unimplemented(
410 "runtime stop session is not implemented",
411 ))
412 }
413
414 async fn prepare_workspace(
416 &self,
417 _request: PrepareRuntimeWorkspaceRequest,
418 ) -> ProviderResult<PrepareRuntimeWorkspaceResponse> {
419 Err(crate::Error::unimplemented(
420 "runtime prepare workspace is not implemented",
421 ))
422 }
423
424 async fn remove_workspace(
426 &self,
427 _request: RemoveRuntimeWorkspaceRequest,
428 ) -> ProviderResult<()> {
429 Err(crate::Error::unimplemented(
430 "runtime remove workspace is not implemented",
431 ))
432 }
433
434 async fn start_app(&self, _request: StartHostedAppRequest) -> ProviderResult<HostedApp> {
436 Err(crate::Error::unimplemented(
437 "runtime start app is not implemented",
438 ))
439 }
440}
441
442#[derive(Clone)]
443pub(crate) struct RuntimeServer<P> {
444 provider: Arc<P>,
445}
446
447impl<P> RuntimeServer<P> {
448 pub(crate) fn new(provider: Arc<P>) -> Self {
449 Self { provider }
450 }
451}
452
453#[async_trait]
454impl<P> pb::runtime_server::Runtime for RuntimeServer<P>
455where
456 P: RuntimeProvider,
457{
458 async fn get_support(
459 &self,
460 request: GrpcRequest<()>,
461 ) -> std::result::Result<GrpcResponse<pb::RuntimeSupport>, Status> {
462 let support = self
463 .provider
464 .get_support(request.into_inner())
465 .await
466 .map_err(|error| rpc_status("runtime get support", error))?;
467 Ok(GrpcResponse::new(support_to_proto(support)))
468 }
469
470 async fn start_session(
471 &self,
472 request: GrpcRequest<pb::StartRuntimeSessionRequest>,
473 ) -> std::result::Result<GrpcResponse<pb::RuntimeSession>, Status> {
474 let session = self
475 .provider
476 .start_session(start_session_request_from_proto(request.into_inner()))
477 .await
478 .map_err(|error| rpc_status("runtime start session", error))?;
479 Ok(GrpcResponse::new(session_to_proto(session)))
480 }
481
482 async fn get_session(
483 &self,
484 request: GrpcRequest<pb::GetRuntimeSessionRequest>,
485 ) -> std::result::Result<GrpcResponse<pb::RuntimeSession>, Status> {
486 let session = self
487 .provider
488 .get_session({
489 let request = request.into_inner();
490 GetRuntimeSessionRequest {
491 session_id: request.session_id,
492 }
493 })
494 .await
495 .map_err(|error| rpc_status("runtime get session", error))?;
496 Ok(GrpcResponse::new(session_to_proto(session)))
497 }
498
499 async fn list_sessions(
500 &self,
501 request: GrpcRequest<pb::ListRuntimeSessionsRequest>,
502 ) -> std::result::Result<GrpcResponse<pb::ListRuntimeSessionsResponse>, Status> {
503 let response = self
504 .provider
505 .list_sessions(list_sessions_request_from_proto(request.into_inner())?)
506 .await
507 .map_err(|error| rpc_status("runtime list sessions", error))?;
508 Ok(GrpcResponse::new(list_sessions_response_to_proto(response)))
509 }
510
511 async fn stop_session(
512 &self,
513 request: GrpcRequest<pb::StopRuntimeSessionRequest>,
514 ) -> std::result::Result<GrpcResponse<()>, Status> {
515 self.provider
516 .stop_session({
517 let request = request.into_inner();
518 StopRuntimeSessionRequest {
519 session_id: request.session_id,
520 }
521 })
522 .await
523 .map_err(|error| rpc_status("runtime stop session", error))?;
524 Ok(GrpcResponse::new(()))
525 }
526
527 async fn prepare_workspace(
528 &self,
529 request: GrpcRequest<pb::PrepareRuntimeWorkspaceRequest>,
530 ) -> std::result::Result<GrpcResponse<pb::PrepareRuntimeWorkspaceResponse>, Status> {
531 let response = self
532 .provider
533 .prepare_workspace(prepare_workspace_request_from_proto(request.into_inner()))
534 .await
535 .map_err(|error| rpc_status("runtime prepare workspace", error))?;
536 Ok(GrpcResponse::new(prepare_workspace_response_to_proto(
537 response,
538 )))
539 }
540
541 async fn remove_workspace(
542 &self,
543 request: GrpcRequest<pb::RemoveRuntimeWorkspaceRequest>,
544 ) -> std::result::Result<GrpcResponse<()>, Status> {
545 self.provider
546 .remove_workspace({
547 let request = request.into_inner();
548 RemoveRuntimeWorkspaceRequest {
549 session_id: request.session_id,
550 agent_session_id: request.agent_session_id,
551 }
552 })
553 .await
554 .map_err(|error| rpc_status("runtime remove workspace", error))?;
555 Ok(GrpcResponse::new(()))
556 }
557
558 async fn start_app(
559 &self,
560 request: GrpcRequest<pb::StartHostedAppRequest>,
561 ) -> std::result::Result<GrpcResponse<pb::HostedApp>, Status> {
562 let app = self
563 .provider
564 .start_app(start_app_request_from_proto(request.into_inner()))
565 .await
566 .map_err(|error| rpc_status("runtime start app", error))?;
567 Ok(GrpcResponse::new(hosted_app_to_proto(app)))
568 }
569}