Skip to main content

gestalt/
runtime_provider_impl.rs

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)]
16/// Native enum for `gestalt.provider.v1.RuntimeEgressMode`.
17pub enum RuntimeEgressMode {
18    #[default]
19    /// The `Unspecified` variant.
20    Unspecified = 0,
21    /// The `None` variant.
22    None = 1,
23    /// The `Cidr` variant.
24    Cidr = 2,
25    /// The `Hostname` variant.
26    Hostname = 3,
27}
28
29impl RuntimeEgressMode {
30    /// Returns the wire integer for this value.
31    pub const fn as_i32(self) -> i32 {
32        self as i32
33    }
34
35    /// Converts a wire integer, mapping unknown values to the unspecified
36    /// variant.
37    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)]
48/// Native message type for `gestalt.provider.v1.RuntimeSupport`.
49pub struct RuntimeSupport {
50    /// The `can_host_apps` field.
51    pub can_host_apps: bool,
52    /// The `egress_mode` field.
53    pub egress_mode: RuntimeEgressMode,
54    /// The `supports_prepare_workspace` field.
55    pub supports_prepare_workspace: bool,
56}
57
58#[derive(Clone, Debug, Default, PartialEq)]
59/// Native message type for `gestalt.provider.v1.RuntimeSession`.
60pub struct RuntimeSession {
61    /// The `id` field.
62    pub id: String,
63    /// The `state` field.
64    pub state: String,
65    /// The `metadata` field.
66    pub metadata: std::collections::BTreeMap<String, String>,
67    /// The `lifecycle` field.
68    pub lifecycle: Option<RuntimeSessionLifecycle>,
69    /// The `state_reason` field.
70    pub state_reason: String,
71    /// The `state_message` field.
72    pub state_message: String,
73}
74
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76/// Native message type for `gestalt.provider.v1.RuntimeSessionLifecycle`.
77pub struct RuntimeSessionLifecycle {
78    /// The `started_at` field.
79    pub started_at: Option<SystemTime>,
80    /// The `recommended_drain_at` field.
81    pub recommended_drain_at: Option<SystemTime>,
82    /// The `expires_at` field.
83    pub expires_at: Option<SystemTime>,
84}
85
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
87/// Native message type for `gestalt.provider.v1.RuntimeImagePullAuth`.
88pub struct RuntimeImagePullAuth {
89    /// The `docker_config_json` field.
90    pub docker_config_json: String,
91}
92
93#[derive(Clone, Debug, Default, PartialEq)]
94/// Native message type for `gestalt.provider.v1.StartRuntimeSessionRequest`.
95pub struct StartRuntimeSessionRequest {
96    /// The `app_name` field.
97    pub app_name: String,
98    /// The `template` field.
99    pub template: String,
100    /// The `image` field.
101    pub image: String,
102    /// The `metadata` field.
103    pub metadata: std::collections::BTreeMap<String, String>,
104    /// The `image_pull_auth` field.
105    pub image_pull_auth: Option<RuntimeImagePullAuth>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
109/// Native message type for `gestalt.provider.v1.GetRuntimeSessionRequest`.
110pub struct GetRuntimeSessionRequest {
111    /// The `session_id` field.
112    pub session_id: String,
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
116/// Native message type for `gestalt.provider.v1.ListRuntimeSessionsRequest`.
117pub struct ListRuntimeSessionsRequest {
118    /// The `page_size` field.
119    pub page_size: i32,
120    /// The `page_token` field.
121    pub page_token: String,
122}
123
124#[derive(Clone, Debug, Default, PartialEq)]
125/// Native message type for `gestalt.provider.v1.ListRuntimeSessionsResponse`.
126pub struct ListRuntimeSessionsResponse {
127    /// The `sessions` field.
128    pub sessions: Vec<RuntimeSession>,
129    /// The `next_page_token` field.
130    pub next_page_token: String,
131}
132
133#[derive(Clone, Debug, Default, PartialEq, Eq)]
134/// Native message type for `gestalt.provider.v1.StopRuntimeSessionRequest`.
135pub struct StopRuntimeSessionRequest {
136    /// The `session_id` field.
137    pub session_id: String,
138}
139
140#[derive(Clone, Debug, Default, PartialEq)]
141/// Native message type for `gestalt.provider.v1.PrepareRuntimeWorkspaceRequest`.
142pub struct PrepareRuntimeWorkspaceRequest {
143    /// The `session_id` field.
144    pub session_id: String,
145    /// The `agent_session_id` field.
146    pub agent_session_id: String,
147    /// The `workspace` field.
148    pub workspace: Option<AgentWorkspace>,
149}
150
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152/// Native message type for `gestalt.provider.v1.PrepareRuntimeWorkspaceResponse`.
153pub struct PrepareRuntimeWorkspaceResponse {
154    /// The `workspace` field.
155    pub workspace: Option<AgentPreparedWorkspace>,
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Eq)]
159/// Native message type for `gestalt.provider.v1.RemoveRuntimeWorkspaceRequest`.
160pub struct RemoveRuntimeWorkspaceRequest {
161    /// The `session_id` field.
162    pub session_id: String,
163    /// The `agent_session_id` field.
164    pub agent_session_id: String,
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq)]
168/// Native message type for `gestalt.provider.v1.StartHostedAppRequest`.
169pub struct StartHostedAppRequest {
170    /// The `session_id` field.
171    pub session_id: String,
172    /// The `app_name` field.
173    pub app_name: String,
174    /// The `command` field.
175    pub command: String,
176    /// The `args` field.
177    pub args: Vec<String>,
178    /// The `env` field.
179    pub env: std::collections::BTreeMap<String, String>,
180    /// The `allowed_hosts` field.
181    pub allowed_hosts: Vec<String>,
182    /// The `default_action` field.
183    pub default_action: String,
184    /// The `host_binary` field.
185    pub host_binary: String,
186    /// The `workdir` field.
187    pub workdir: String,
188}
189
190#[derive(Clone, Debug, Default, PartialEq, Eq)]
191/// Native message type for `gestalt.provider.v1.HostedApp`.
192pub struct HostedApp {
193    /// The `id` field.
194    pub id: String,
195    /// The `session_id` field.
196    pub session_id: String,
197    /// The `app_name` field.
198    pub app_name: String,
199    /// The `dial_target` field.
200    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]
334/// Provider trait for serving hosted runtime sessions.
335pub trait RuntimeProvider: Send + Sync + 'static {
336    /// Configures the provider before it starts serving requests.
337    async fn configure(
338        &self,
339        _name: &str,
340        _config: serde_json::Map<String, serde_json::Value>,
341    ) -> ProviderResult<()> {
342        Ok(())
343    }
344
345    /// Returns runtime metadata that should augment the static manifest.
346    fn metadata(&self) -> Option<RuntimeMetadata> {
347        None
348    }
349
350    /// Returns non-fatal warnings the host should surface to users.
351    fn warnings(&self) -> Vec<String> {
352        Vec::new()
353    }
354
355    /// Performs an optional health check.
356    async fn health_check(&self) -> ProviderResult<()> {
357        Ok(())
358    }
359
360    /// Starts provider-owned background work after configuration.
361    async fn start(&self) -> ProviderResult<()> {
362        Ok(())
363    }
364
365    /// Shuts the provider down before the runtime exits.
366    async fn close(&self) -> ProviderResult<()> {
367        Ok(())
368    }
369
370    /// Returns the runtime capabilities supported by this provider.
371    async fn get_support(&self, _request: ()) -> ProviderResult<RuntimeSupport> {
372        Err(crate::Error::unimplemented(
373            "runtime get support is not implemented",
374        ))
375    }
376
377    /// Starts a hosted runtime session.
378    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    /// Returns one hosted runtime session by ID.
388    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    /// Lists hosted runtime sessions.
398    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    /// Stops a hosted runtime session.
408    async fn stop_session(&self, _request: StopRuntimeSessionRequest) -> ProviderResult<()> {
409        Err(crate::Error::unimplemented(
410            "runtime stop session is not implemented",
411        ))
412    }
413
414    /// Prepares an agent workspace for use by a hosted app.
415    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    /// Removes a previously prepared agent workspace.
425    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    /// Starts one hosted app process inside a runtime session.
435    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}