diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 7a7e8af1b..1be456f61 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -2653,6 +2653,49 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + /api/v1/runs/{id}/sandbox/vnc: + post: + operationId: createSandboxVncPreview + tags: [Human-in-the-Loop] + summary: Create Sandbox VNC Preview + description: Starts or ensures Daytona Computer Use for the run sandbox and returns a signed noVNC preview URL. + parameters: + - $ref: "#/components/parameters/RunId" + responses: + "201": + description: Signed noVNC preview URL created + content: + application/json: + schema: + $ref: "#/components/schemas/VncPreviewResponse" + "404": + description: Run not found + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "409": + description: Run has no active sandbox, Computer Use startup failed, or signed preview generation failed + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "501": + description: Sandbox provider does not support VNC previews + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /api/v1/runs/{id}/sandbox/files: get: operationId: listSandboxFiles @@ -7910,6 +7953,35 @@ components: items: $ref: "#/components/schemas/SandboxFileEntry" + VncPreviewResponse: + description: Response containing a signed noVNC preview URL for a Daytona sandbox. + type: object + required: + - url + - provider + - port + - expires_in_secs + properties: + url: + type: string + description: Signed noVNC preview URL. + example: "https://preview.example.com/sb-a1b2c3d4/6080?token=..." + provider: + type: string + description: Sandbox provider that produced the VNC preview. + example: daytona + port: + type: integer + minimum: 1 + maximum: 65535 + description: noVNC port exposed by the sandbox. + example: 6080 + expires_in_secs: + type: integer + minimum: 1 + description: Signed URL time-to-live in seconds. + example: 3600 + # ── Insights Schemas ───────────────────────────────────────────────── SavedQuery: diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index fdaca48ed..4f553601a 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -36,7 +36,8 @@ pub use fabro_api::types::{ RunError, RunManifest, RunStage, RunStatusResponse, SandboxDetails, SandboxFileEntry, SandboxFileListResponse, SshAccessRequest, SshAccessResponse, StageHandler, StageState, StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRepairRunIssue, - SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse, WriteBlobResponse, + SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse, VncPreviewResponse, + WriteBlobResponse, }; use fabro_auth::{ CredentialSource, VaultCredentialSource, auth_issue_message, parse_credential_secret, diff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs index 1dcd1ce8a..9a4b738bd 100644 --- a/lib/crates/fabro-server/src/server/handler/sandbox.rs +++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs @@ -1,18 +1,59 @@ +use std::num::NonZeroU64; use std::sync::Arc; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; use fabro_sandbox::{TerminalSize, open_terminal_for_run}; +use futures_util::FutureExt; +use futures_util::future::BoxFuture; use super::super::{ ApiError, AppState, Bytes, DaytonaSandbox, EnvVars, HeaderMap, IntoResponse, Json, NamedTempFile, Path, PreviewUrlRequest, PreviewUrlResponse, Query, RequiredUser, Response, Router, RunId, Sandbox, SandboxDetails, SandboxFileEntry, SandboxFileListResponse, - SandboxProvider, SshAccessRequest, SshAccessResponse, State, StatusCode, collect_causes, fs, - get, octet_stream_response, parse_run_id_path, post, reconnect_for_run, reject_if_archived, - render_with_causes, sandbox_details, + SandboxProvider, SshAccessRequest, SshAccessResponse, State, StatusCode, VncPreviewResponse, + collect_causes, fs, get, octet_stream_response, parse_run_id_path, post, reconnect_for_run, + reject_if_archived, render_with_causes, sandbox_details, }; const MAX_TERMINAL_CONTROL_BYTES: usize = 4096; +const DEFAULT_VNC_NO_VNC_PORT: u16 = 6080; +const DEFAULT_VNC_TTL_SECS: i32 = 3600; + +trait VncSandbox { + fn start_computer_use(&self) -> BoxFuture<'_, fabro_sandbox::Result<()>>; + fn signed_preview_url( + &self, + port: u16, + expires_in_secs: i32, + ) -> BoxFuture<'_, fabro_sandbox::Result>; +} + +impl VncSandbox for DaytonaSandbox { + fn start_computer_use(&self) -> BoxFuture<'_, fabro_sandbox::Result<()>> { + async move { + let computer_use = self.computer_use().await?; + computer_use + .start() + .await + .map_err(|err| fabro_sandbox::Error::context("Failed to start Computer Use", err)) + .map(|_| ()) + } + .boxed() + } + + fn signed_preview_url( + &self, + port: u16, + expires_in_secs: i32, + ) -> BoxFuture<'_, fabro_sandbox::Result> { + async move { + self.get_signed_preview_url(port, Some(expires_in_secs)) + .await + .map(|preview| preview.url) + } + .boxed() + } +} pub(super) fn routes() -> Router> { Router::new() @@ -20,6 +61,7 @@ pub(super) fn routes() -> Router> { .route("/runs/{id}/ssh", post(create_ssh_access)) .route("/runs/{id}/terminal", get(run_terminal)) .route("/runs/{id}/sandbox", get(retrieve_run_sandbox)) + .route("/runs/{id}/sandbox/vnc", post(create_sandbox_vnc_preview)) .route("/runs/{id}/sandbox/files", get(list_sandbox_files)) .route( "/runs/{id}/sandbox/file", @@ -374,6 +416,60 @@ async fn create_ssh_access( } } +async fn create_sandbox_vnc_preview( + _auth: RequiredUser, + State(state): State>, + Path(id): Path, +) -> Response { + let id = match parse_run_id_path(&id) { + Ok(id) => id, + Err(response) => return response, + }; + let record = match load_run_sandbox_record(&state, &id).await { + Ok(record) => record, + Err(response) => return response, + }; + if record.provider != SandboxProvider::Daytona.to_string() { + return ApiError::new( + StatusCode::NOT_IMPLEMENTED, + "Sandbox provider does not support VNC previews.", + ) + .into_response(); + } + let sandbox = match reconnect_daytona_sandbox(&state, &id).await { + Ok(sandbox) => sandbox, + Err(response) => return response, + }; + match build_vnc_preview_response(&sandbox).await { + Ok(response) => (StatusCode::CREATED, Json(response)).into_response(), + Err(response) => response, + } +} + +async fn build_vnc_preview_response( + sandbox: &impl VncSandbox, +) -> Result { + sandbox.start_computer_use().await.map_err(|err| { + ApiError::new(StatusCode::CONFLICT, err.display_with_causes()).into_response() + })?; + let url = sandbox + .signed_preview_url(DEFAULT_VNC_NO_VNC_PORT, DEFAULT_VNC_TTL_SECS) + .await + .map_err(|err| { + ApiError::new(StatusCode::CONFLICT, err.display_with_causes()).into_response() + })?; + Ok(VncPreviewResponse { + expires_in_secs: NonZeroU64::new( + u64::try_from(DEFAULT_VNC_TTL_SECS).expect("default VNC TTL should fit in u64"), + ) + .expect("default VNC TTL should be nonzero"), + port: NonZeroU64::new(u64::from(DEFAULT_VNC_NO_VNC_PORT)) + .expect("default VNC port should be nonzero"), + provider: "daytona".to_string(), + url, + }) +} + async fn list_sandbox_files( _auth: RequiredUser, State(state): State>, @@ -579,6 +675,7 @@ async fn load_run_sandbox_record_or_not_found( #[cfg(test)] mod tests { use axum::http::{HeaderMap, HeaderValue}; + use futures_util::FutureExt; use super::*; @@ -623,6 +720,87 @@ mod tests { headers.insert("origin", HeaderValue::from_static("https://evil.example")); assert!(!origin_allowed(&headers)); } + + struct FakeVncSandbox { + start_error: Option<&'static str>, + signed_url_error: Option<&'static str>, + signed_url: &'static str, + } + + impl VncSandbox for FakeVncSandbox { + fn start_computer_use( + &self, + ) -> futures_util::future::BoxFuture<'_, fabro_sandbox::Result<()>> { + async move { + match self.start_error { + Some(message) => Err(fabro_sandbox::Error::message(message)), + None => Ok(()), + } + } + .boxed() + } + + fn signed_preview_url( + &self, + port: u16, + expires_in_secs: i32, + ) -> futures_util::future::BoxFuture<'_, fabro_sandbox::Result> { + async move { + assert_eq!(port, DEFAULT_VNC_NO_VNC_PORT); + assert_eq!(expires_in_secs, DEFAULT_VNC_TTL_SECS); + match self.signed_url_error { + Some(message) => Err(fabro_sandbox::Error::message(message)), + None => Ok(self.signed_url.to_string()), + } + } + .boxed() + } + } + + #[tokio::test] + async fn vnc_preview_response_uses_daytona_defaults() { + let sandbox = FakeVncSandbox { + start_error: None, + signed_url_error: None, + signed_url: "https://preview.example.test/sandbox/6080", + }; + + let response = build_vnc_preview_response(&sandbox).await.unwrap(); + + assert_eq!(response.url, "https://preview.example.test/sandbox/6080"); + assert_eq!(response.provider, "daytona"); + assert_eq!(response.port.get(), u64::from(DEFAULT_VNC_NO_VNC_PORT)); + assert_eq!( + response.expires_in_secs.get(), + u64::try_from(DEFAULT_VNC_TTL_SECS).unwrap() + ); + } + + #[tokio::test] + async fn vnc_preview_response_maps_computer_use_start_failure_to_conflict() { + let sandbox = FakeVncSandbox { + start_error: Some("computer use failed"), + signed_url_error: None, + signed_url: "https://preview.example.test/sandbox/6080", + }; + + let response = build_vnc_preview_response(&sandbox).await.unwrap_err(); + + assert_eq!(response.status(), StatusCode::CONFLICT); + } + + #[tokio::test] + async fn vnc_preview_response_maps_signed_preview_failure_to_conflict() { + let sandbox = FakeVncSandbox { + start_error: None, + signed_url_error: Some("preview failed"), + signed_url: "https://preview.example.test/sandbox/6080", + }; + + let response = build_vnc_preview_response(&sandbox).await.unwrap_err(); + + assert_eq!(response.status(), StatusCode::CONFLICT); + } } #[cfg(test)] @@ -643,6 +821,14 @@ mod retrieve_sandbox_tests { .expect("sandbox details GET request should build") } + fn req_post(uri: &str) -> Request { + Request::builder() + .method("POST") + .uri(uri) + .body(Body::empty()) + .expect("sandbox POST request should build") + } + async fn body_json(response: axum::response::Response) -> Value { let bytes = to_bytes(response.into_body(), usize::MAX) .await @@ -669,6 +855,28 @@ mod retrieve_sandbox_tests { run_store.append_event(&payload).await.unwrap(); } + async fn append_sandbox_initialized( + run_store: &fabro_store::RunDatabase, + run_id: &RunId, + provider: &str, + ) { + let payload = fabro_store::EventPayload::new( + json!({ + "id": "evt-sandbox-init", + "ts": "2026-05-09T12:00:00Z", + "run_id": run_id, + "event": "sandbox.initialized", + "properties": { + "provider": provider, + "working_directory": "/workspace", + }, + }), + run_id, + ) + .expect("sandbox.initialized payload should validate"); + run_store.append_event(&payload).await.unwrap(); + } + #[tokio::test] async fn missing_run_returns_404() { let app = build_test_router(test_app_state()); @@ -725,21 +933,7 @@ mod retrieve_sandbox_tests { .await .expect("test run should be creatable"); append_run_created(&run_store, &run_id).await; - let payload = fabro_store::EventPayload::new( - json!({ - "id": "evt-sandbox-init", - "ts": "2026-05-09T12:00:00Z", - "run_id": run_id, - "event": "sandbox.initialized", - "properties": { - "provider": "local", - "working_directory": "/workspace", - }, - }), - &run_id, - ) - .expect("sandbox.initialized payload should validate"); - run_store.append_event(&payload).await.unwrap(); + append_sandbox_initialized(&run_store, &run_id, "local").await; let response = app .oneshot(req_get(&format!("/api/v1/runs/{run_id}/sandbox"))) @@ -764,21 +958,7 @@ mod retrieve_sandbox_tests { .await .expect("test run should be creatable"); append_run_created(&run_store, &run_id).await; - let payload = fabro_store::EventPayload::new( - json!({ - "id": "evt-sandbox-init", - "ts": "2026-05-09T12:00:00Z", - "run_id": run_id, - "event": "sandbox.initialized", - "properties": { - "provider": "ephemeral-mystery-cloud", - "working_directory": "/workspace", - }, - }), - &run_id, - ) - .expect("sandbox.initialized payload should validate"); - run_store.append_event(&payload).await.unwrap(); + append_sandbox_initialized(&run_store, &run_id, "ephemeral-mystery-cloud").await; let response = app .oneshot(req_get(&format!("/api/v1/runs/{run_id}/sandbox"))) @@ -786,4 +966,46 @@ mod retrieve_sandbox_tests { .unwrap(); assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); } + + #[tokio::test] + async fn local_sandbox_vnc_returns_501() { + let state = test_app_state(); + let app = build_test_router(state.clone()); + let run_id = RunId::new(); + let run_store = state + .store_ref() + .create_run(&run_id) + .await + .expect("test run should be creatable"); + append_run_created(&run_store, &run_id).await; + append_sandbox_initialized(&run_store, &run_id, "local").await; + + let response = app + .oneshot(req_post(&format!("/api/v1/runs/{run_id}/sandbox/vnc"))) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); + } + + #[tokio::test] + async fn docker_sandbox_vnc_returns_501_without_reconnect() { + let state = test_app_state(); + let app = build_test_router(state.clone()); + let run_id = RunId::new(); + let run_store = state + .store_ref() + .create_run(&run_id) + .await + .expect("test run should be creatable"); + append_run_created(&run_store, &run_id).await; + append_sandbox_initialized(&run_store, &run_id, "docker").await; + + let response = app + .oneshot(req_post(&format!("/api/v1/runs/{run_id}/sandbox/vnc"))) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); + } } diff --git a/lib/crates/fabro-server/tests/it/api/mod.rs b/lib/crates/fabro-server/tests/it/api/mod.rs index 9bec16c6c..01e8f3f80 100644 --- a/lib/crates/fabro-server/tests/it/api/mod.rs +++ b/lib/crates/fabro-server/tests/it/api/mod.rs @@ -5,6 +5,7 @@ mod install_openai_compatible; mod routing; mod run_files; mod runs; +mod sandbox_vnc; mod settings; mod system; mod tcp; diff --git a/lib/crates/fabro-server/tests/it/api/sandbox_vnc.rs b/lib/crates/fabro-server/tests/it/api/sandbox_vnc.rs new file mode 100644 index 000000000..7ba45170e --- /dev/null +++ b/lib/crates/fabro-server/tests/it/api/sandbox_vnc.rs @@ -0,0 +1,58 @@ +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use tower::ServiceExt; + +use crate::helpers::{ + MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status, test_app_state, +}; + +#[tokio::test] +async fn vnc_for_missing_run_returns_not_found() { + let app = fabro_server::test_support::build_test_router(test_app_state()); + let fake = "01ARZ3NDEKTSV4RRFFQ69G5FAV"; + let req = Request::builder() + .method("POST") + .uri(api(&format!("/runs/{fake}/sandbox/vnc"))) + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + + response_status( + response, + StatusCode::NOT_FOUND, + "POST /api/v1/runs/{id}/sandbox/vnc", + ) + .await; +} + +#[tokio::test] +async fn vnc_for_run_without_sandbox_returns_conflict() { + let app = fabro_server::test_support::build_test_router(test_app_state()); + let create_req = Request::builder() + .method("POST") + .uri(api("/runs")) + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&minimal_manifest_json(MINIMAL_DOT)).unwrap(), + )) + .unwrap(); + let create_response = app.clone().oneshot(create_req).await.unwrap(); + let create_body = + response_json(create_response, StatusCode::CREATED, "POST /api/v1/runs").await; + let run_id = create_body["id"].as_str().unwrap(); + let req = Request::builder() + .method("POST") + .uri(api(&format!("/runs/{run_id}/sandbox/vnc"))) + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + + response_status( + response, + StatusCode::CONFLICT, + format!("POST /api/v1/runs/{run_id}/sandbox/vnc"), + ) + .await; +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 4963027cd..c9607975e 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -346,6 +346,7 @@ models/tls-mode.ts models/update-run-request.ts models/user-response.ts models/validate-response.ts +models/vnc-preview-response.ts models/webhook-strategy.ts models/workflow-detail-response.ts models/workflow-diagnostic.ts diff --git a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts index ca30e2ec5..6ce17da88 100644 --- a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts +++ b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts @@ -41,6 +41,8 @@ import type { SshAccessResponse } from '../models'; import type { SteerRunRequest } from '../models'; // @ts-ignore import type { SubmitAnswerRequest } from '../models'; +// @ts-ignore +import type { VncPreviewResponse } from '../models'; /** * HumanInTheLoopApi - axios parameter creator */ @@ -91,6 +93,46 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf options: localVarRequestOptions, }; }, + /** + * Starts or ensures Daytona Computer Use for the run sandbox and returns a signed noVNC preview URL. + * @summary Create Sandbox VNC Preview + * @param {string} id Unique run identifier (ULID). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSandboxVncPreview: async (id: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('createSandboxVncPreview', 'id', id) + const localVarPath = `/api/v1/runs/{id}/sandbox/vnc` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Generates a preview URL for a port exposed by the run\'s sandbox environment. * @summary Preview URL @@ -534,6 +576,19 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.createRunSshAccess']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Starts or ensures Daytona Computer Use for the run sandbox and returns a signed noVNC preview URL. + * @summary Create Sandbox VNC Preview + * @param {string} id Unique run identifier (ULID). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createSandboxVncPreview(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createSandboxVncPreview(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.createSandboxVncPreview']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Generates a preview URL for a port exposed by the run\'s sandbox environment. * @summary Preview URL @@ -682,6 +737,16 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration, createRunSshAccess(id: string, sshAccessRequest: SshAccessRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createRunSshAccess(id, sshAccessRequest, options).then((request) => request(axios, basePath)); }, + /** + * Starts or ensures Daytona Computer Use for the run sandbox and returns a signed noVNC preview URL. + * @summary Create Sandbox VNC Preview + * @param {string} id Unique run identifier (ULID). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSandboxVncPreview(id: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.createSandboxVncPreview(id, options).then((request) => request(axios, basePath)); + }, /** * Generates a preview URL for a port exposed by the run\'s sandbox environment. * @summary Preview URL @@ -802,6 +867,17 @@ export class HumanInTheLoopApi extends BaseAPI { return HumanInTheLoopApiFp(this.configuration).createRunSshAccess(id, sshAccessRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Starts or ensures Daytona Computer Use for the run sandbox and returns a signed noVNC preview URL. + * @summary Create Sandbox VNC Preview + * @param {string} id Unique run identifier (ULID). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public createSandboxVncPreview(id: string, options?: RawAxiosRequestConfig) { + return HumanInTheLoopApiFp(this.configuration).createSandboxVncPreview(id, options).then((request) => request(this.axios, this.basePath)); + } + /** * Generates a preview URL for a port exposed by the run\'s sandbox environment. * @summary Preview URL diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 9b80cf65b..8ed99846f 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -323,6 +323,7 @@ export * from './tls-mode'; export * from './update-run-request'; export * from './user-response'; export * from './validate-response'; +export * from './vnc-preview-response'; export * from './webhook-strategy'; export * from './workflow-detail-response'; export * from './workflow-diagnostic'; diff --git a/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts b/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts new file mode 100644 index 000000000..1fa77e63c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Response containing a signed noVNC preview URL for a Daytona sandbox. + */ +export interface VncPreviewResponse { + /** + * Signed noVNC preview URL. + */ + 'url': string; + /** + * Sandbox provider that produced the VNC preview. + */ + 'provider': string; + /** + * noVNC port exposed by the sandbox. + */ + 'port': number; + /** + * Signed URL time-to-live in seconds. + */ + 'expires_in_secs': number; +} +