runtime: own working directories

This commit is contained in:
Keisuke Hirata 2026-07-09 19:04:37 +09:00
parent afc43780a7
commit fa233ba025
No known key found for this signature in database
9 changed files with 798 additions and 117 deletions

View File

@ -1,11 +1,11 @@
use crate::catalog::{CreateWorkerRequest, WorkingDirectoryStatus}; use crate::catalog::{CreateWorkerRequest, WorkingDirectoryRequest, WorkingDirectoryStatus};
use crate::config_bundle::ConfigBundle; use crate::config_bundle::ConfigBundle;
use crate::error::RuntimeError; use crate::error::RuntimeError;
use crate::identity::WorkerRef; use crate::identity::WorkerRef;
use crate::interaction::WorkerInput; use crate::interaction::WorkerInput;
#[cfg(feature = "ws-server")] #[cfg(feature = "ws-server")]
use crate::observation::WorkerObservationEvent; use crate::observation::WorkerObservationEvent;
use crate::working_directory::WorkingDirectoryBinding; use crate::working_directory::{WorkingDirectoryBinding, WorkingDirectoryDiagnostic};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
@ -313,6 +313,42 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static {
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult; fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult;
fn create_working_directory(
&self,
_request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_unsupported",
"Worker execution backend does not support working directory materialization",
))
}
fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
Vec::new()
}
fn working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_unknown",
format!("working directory `{working_directory_id}` is not known to this Runtime"),
))
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_cleanup_unsupported",
format!(
"working directory `{working_directory_id}` cannot be cleaned up by this backend"
),
))
}
fn dispatch_input( fn dispatch_input(
&self, &self,
handle: &WorkerExecutionHandle, handle: &WorkerExecutionHandle,
@ -358,6 +394,31 @@ impl WorkerExecutionBackendRef {
self.backend.spawn_worker(request) self.backend.spawn_worker(request)
} }
pub(crate) fn create_working_directory(
&self,
request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
self.backend.create_working_directory(request)
}
pub(crate) fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
self.backend.list_working_directories()
}
pub(crate) fn working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
self.backend.working_directory(working_directory_id)
}
pub(crate) fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
self.backend.cleanup_working_directory(working_directory_id)
}
pub(crate) fn dispatch_input( pub(crate) fn dispatch_input(
&self, &self,
handle: &WorkerExecutionHandle, handle: &WorkerExecutionHandle,

View File

@ -9,6 +9,7 @@
use crate::Runtime; use crate::Runtime;
use crate::catalog::{ use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, WorkerDetail, WorkerLifecycleAck, WorkerSummary, ConfigBundleRef, CreateWorkerRequest, WorkerDetail, WorkerLifecycleAck, WorkerSummary,
WorkingDirectoryRequest, WorkingDirectoryStatus,
}; };
use crate::config_bundle::{ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary}; use crate::config_bundle::{ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary};
use crate::error::RuntimeError; use crate::error::RuntimeError;
@ -138,6 +139,14 @@ pub fn runtime_http_router(runtime: Runtime, local_token: Option<String>) -> Rou
"/v1/config-bundles/{bundle_id}/availability", "/v1/config-bundles/{bundle_id}/availability",
get(check_config_bundle), get(check_config_bundle),
) )
.route(
"/v1/working-directories",
get(list_working_directories).post(create_working_directory),
)
.route(
"/v1/working-directories/{working_directory_id}",
get(get_working_directory).delete(cleanup_working_directory),
)
.route("/v1/workers", get(list_workers).post(create_worker)) .route("/v1/workers", get(list_workers).post(create_worker))
.route("/v1/workers/{worker_id}", get(get_worker)) .route("/v1/workers/{worker_id}", get(get_worker))
.route("/v1/workers/{worker_id}/input", post(send_worker_input)) .route("/v1/workers/{worker_id}/input", post(send_worker_input))
@ -197,6 +206,18 @@ pub struct RuntimeHttpWorkersResponse {
pub workers: Vec<WorkerSummary>, pub workers: Vec<WorkerSummary>,
} }
/// `GET /v1/working-directories` response.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHttpWorkingDirectoriesResponse {
pub working_directories: Vec<WorkingDirectoryStatus>,
}
/// Working directory response used by create/detail/delete endpoints.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHttpWorkingDirectoryResponse {
pub working_directory: WorkingDirectoryStatus,
}
/// Worker detail response used by create/detail endpoints. /// Worker detail response used by create/detail endpoints.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHttpWorkerResponse { pub struct RuntimeHttpWorkerResponse {
@ -399,6 +420,58 @@ async fn list_workers(
Ok(Json(RuntimeHttpWorkersResponse { workers })) Ok(Json(RuntimeHttpWorkersResponse { workers }))
} }
async fn list_working_directories(
State(state): State<RuntimeHttpState>,
) -> RestResult<RuntimeHttpWorkingDirectoriesResponse> {
let working_directories = state
.runtime
.list_working_directories()
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpWorkingDirectoriesResponse {
working_directories,
}))
}
async fn create_working_directory(
State(state): State<RuntimeHttpState>,
body: Result<Json<WorkingDirectoryRequest>, JsonRejection>,
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
let Json(request) = body.map_err(RuntimeHttpRestError::json_rejection)?;
let working_directory = state
.runtime
.create_working_directory(request)
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
working_directory,
}))
}
async fn get_working_directory(
State(state): State<RuntimeHttpState>,
Path(working_directory_id): Path<String>,
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
let working_directory = state
.runtime
.working_directory(&working_directory_id)
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
working_directory,
}))
}
async fn cleanup_working_directory(
State(state): State<RuntimeHttpState>,
Path(working_directory_id): Path<String>,
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
let working_directory = state
.runtime
.cleanup_working_directory(&working_directory_id)
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
working_directory,
}))
}
async fn get_worker( async fn get_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
Path(worker_id): Path<String>, Path(worker_id): Path<String>,

View File

@ -1,6 +1,7 @@
use crate::catalog::{ use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail, WorkerLifecycleAck, ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail, WorkerLifecycleAck,
WorkerStatus, WorkerSummary, WorkingDirectoryStatus as CatalogWorkingDirectoryStatus, WorkerStatus, WorkerSummary, WorkingDirectoryRequest,
WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
}; };
use crate::config_bundle::{ use crate::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle, ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle,
@ -234,6 +235,79 @@ impl Runtime {
Ok(event_id) Ok(event_id)
} }
/// Create a Runtime-owned working directory through the attached execution backend.
pub fn create_working_directory(
&self,
request: WorkingDirectoryRequest,
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
let backend = {
let state = self.lock()?;
state.ensure_running()?;
state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "working directory creation requires an execution backend".to_string(),
}
})?
};
backend
.create_working_directory(&request)
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
}
/// List Runtime-owned working directories through the attached execution backend.
pub fn list_working_directories(
&self,
) -> Result<Vec<CatalogWorkingDirectoryStatus>, RuntimeError> {
let backend = {
let state = self.lock()?;
state.ensure_running()?;
state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "working directory listing requires an execution backend".to_string(),
}
})?
};
Ok(backend.list_working_directories())
}
/// Get a Runtime-owned working directory status.
pub fn working_directory(
&self,
working_directory_id: &str,
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
let backend = {
let state = self.lock()?;
state.ensure_running()?;
state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "working directory lookup requires an execution backend".to_string(),
}
})?
};
backend
.working_directory(working_directory_id)
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
}
/// Cleanup a Runtime-owned working directory.
pub fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
let backend = {
let state = self.lock()?;
state.ensure_running()?;
state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "working directory cleanup requires an execution backend".to_string(),
}
})?
};
backend
.cleanup_working_directory(working_directory_id)
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
}
/// Create a Worker through the canonical ConfigBundle + execution backend path. /// Create a Worker through the canonical ConfigBundle + execution backend path.
pub fn create_worker( pub fn create_worker(
&self, &self,

View File

@ -14,6 +14,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc}; use std::sync::{Arc, Mutex, mpsc};
use std::time::Duration; use std::time::Duration;
use crate::catalog::{WorkingDirectoryRequest, WorkingDirectoryStatus};
use crate::config_bundle::verified_profile_source_archive; use crate::config_bundle::verified_profile_source_archive;
use crate::execution::{ use crate::execution::{
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
@ -24,7 +25,9 @@ use crate::resource::{
BackendResourceClient, BackendResourceError, ProfileSourceArchiveCache, BackendResourceClient, BackendResourceError, ProfileSourceArchiveCache,
build_profile_source_archive_fetch_request, profile_source_archive_from_response, build_profile_source_archive_fetch_request, profile_source_archive_from_response,
}; };
use crate::working_directory::{WorkingDirectoryBinding, WorkingDirectoryMaterializer}; use crate::working_directory::{
WorkingDirectoryBinding, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
};
use async_trait::async_trait; use async_trait::async_trait;
use manifest::paths; use manifest::paths;
use protocol::{Method, Segment, WorkerStatus}; use protocol::{Method, Segment, WorkerStatus};
@ -441,6 +444,52 @@ where
&self.backend_id &self.backend_id
} }
fn create_working_directory(
&self,
request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
let Some(materializer) = self.working_directory_materializer.as_ref() else {
return Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_materializer_unavailable",
"working directory materialization requested, but no materializer is configured for this runtime backend",
));
};
Ok(materializer.create(request)?.status())
}
fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
self.working_directory_materializer
.as_ref()
.and_then(|materializer| materializer.list_working_directories().ok())
.unwrap_or_default()
}
fn working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
let Some(materializer) = self.working_directory_materializer.as_ref() else {
return Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_materializer_unavailable",
"working directory lookup requested, but no materializer is configured for this runtime backend",
));
};
materializer.working_directory_status(working_directory_id)
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
let Some(materializer) = self.working_directory_materializer.as_ref() else {
return Err(WorkingDirectoryDiagnostic::rejected(
"working_directory_materializer_unavailable",
"working directory cleanup requested, but no materializer is configured for this runtime backend",
));
};
materializer.cleanup_working_directory(working_directory_id)
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult { fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
if self if self
.workers .workers

View File

@ -95,6 +95,10 @@ pub struct WorkingDirectoryDiagnostic {
} }
impl WorkingDirectoryDiagnostic { impl WorkingDirectoryDiagnostic {
pub fn rejected(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(code, message)
}
fn new(code: impl Into<String>, message: impl Into<String>) -> Self { fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self { Self {
code: code.into(), code: code.into(),

View File

@ -16,7 +16,7 @@ use std::{
use worker_runtime::catalog::{ use worker_runtime::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail as EmbeddedWorkerDetail, ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail as EmbeddedWorkerDetail,
WorkerStatus as EmbeddedWorkerStatus, WorkingDirectoryClaim, WorkingDirectoryRequest, WorkerStatus as EmbeddedWorkerStatus, WorkingDirectoryClaim, WorkingDirectoryRequest,
WorkingDirectorySummary, WorkingDirectoryStatus, WorkingDirectorySummary,
}; };
use worker_runtime::config_bundle::{ use worker_runtime::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleMetadata, ConfigBundleProvenance, ConfigBundle, ConfigBundleAvailability, ConfigBundleMetadata, ConfigBundleProvenance,
@ -32,6 +32,7 @@ use worker_runtime::http_server::{
RuntimeHttpErrorResponse, RuntimeHttpSummaryResponse, RuntimeHttpTranscriptResponse, RuntimeHttpErrorResponse, RuntimeHttpSummaryResponse, RuntimeHttpTranscriptResponse,
RuntimeHttpWorkerInputResponse, RuntimeHttpWorkerLifecycleRequest, RuntimeHttpWorkerInputResponse, RuntimeHttpWorkerLifecycleRequest,
RuntimeHttpWorkerLifecycleResponse, RuntimeHttpWorkerResponse, RuntimeHttpWorkersResponse, RuntimeHttpWorkerLifecycleResponse, RuntimeHttpWorkerResponse, RuntimeHttpWorkersResponse,
RuntimeHttpWorkingDirectoriesResponse, RuntimeHttpWorkingDirectoryResponse,
}; };
use worker_runtime::identity::{ use worker_runtime::identity::{
RuntimeId as EmbeddedRuntimeId, WorkerId as EmbeddedWorkerId, WorkerRef as EmbeddedWorkerRef, RuntimeId as EmbeddedRuntimeId, WorkerId as EmbeddedWorkerId, WorkerRef as EmbeddedWorkerRef,
@ -266,6 +267,14 @@ pub struct WorkerLookupResult {
pub diagnostics: Vec<RuntimeDiagnostic>, pub diagnostics: Vec<RuntimeDiagnostic>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeWorkingDirectoryResult {
pub state: WorkerOperationState,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_directory: Option<WorkingDirectoryStatus>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
/// Browser-safe worker spawn request shape. /// Browser-safe worker spawn request shape.
/// ///
/// The request carries Browser-facing launch semantics only: workspace intent, /// The request carries Browser-facing launch semantics only: workspace intent,
@ -537,6 +546,56 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
fn worker(&self, worker_id: &str) -> WorkerLookupResult; fn worker(&self, worker_id: &str) -> WorkerLookupResult;
fn create_working_directory(
&self,
_request: WorkingDirectoryRequest,
) -> RuntimeWorkingDirectoryResult {
RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Unsupported,
working_directory: None,
diagnostics: vec![diagnostic(
"runtime_working_directory_create_unsupported",
DiagnosticSeverity::Info,
"runtime does not implement working directory creation".to_string(),
)],
}
}
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
RuntimeList::new(Vec::new(), Vec::new())
}
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Unsupported,
working_directory: None,
diagnostics: vec![diagnostic(
"runtime_working_directory_lookup_unsupported",
DiagnosticSeverity::Info,
format!(
"runtime does not implement working directory lookup for `{working_directory_id}`"
),
)],
}
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> RuntimeWorkingDirectoryResult {
RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Unsupported,
working_directory: None,
diagnostics: vec![diagnostic(
"runtime_working_directory_cleanup_unsupported",
DiagnosticSeverity::Info,
format!(
"runtime does not implement working directory cleanup for `{working_directory_id}`"
),
)],
}
}
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult { fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
WorkerSpawnResult { WorkerSpawnResult {
state: WorkerOperationState::Unsupported, state: WorkerOperationState::Unsupported,
@ -882,6 +941,47 @@ impl RuntimeRegistry {
Ok(runtime.spawn_worker(request)) Ok(runtime.spawn_worker(request))
} }
pub fn create_working_directory(
&self,
runtime_id: &str,
request: WorkingDirectoryRequest,
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", runtime_id)?;
let runtime = self.runtime(runtime_id)?;
Ok(runtime.create_working_directory(request))
}
pub fn list_working_directories(
&self,
runtime_id: &str,
) -> Result<RuntimeList<WorkingDirectoryStatus>, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", runtime_id)?;
let runtime = self.runtime(runtime_id)?;
Ok(runtime.list_working_directories())
}
pub fn working_directory(
&self,
runtime_id: &str,
working_directory_id: &str,
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", runtime_id)?;
validate_backend_identifier("working_directory_id", working_directory_id)?;
let runtime = self.runtime(runtime_id)?;
Ok(runtime.working_directory(working_directory_id))
}
pub fn cleanup_working_directory(
&self,
runtime_id: &str,
working_directory_id: &str,
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", runtime_id)?;
validate_backend_identifier("working_directory_id", working_directory_id)?;
let runtime = self.runtime(runtime_id)?;
Ok(runtime.cleanup_working_directory(working_directory_id))
}
pub fn sync_config_bundle( pub fn sync_config_bundle(
&self, &self,
runtime_id: &str, runtime_id: &str,
@ -1311,6 +1411,64 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
} }
} }
fn create_working_directory(
&self,
request: WorkingDirectoryRequest,
) -> RuntimeWorkingDirectoryResult {
match self.runtime.create_working_directory(request) {
Ok(working_directory) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(working_directory),
diagnostics: Vec::new(),
},
Err(err) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![embedded_runtime_diagnostic(&err)],
},
}
}
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
match self.runtime.list_working_directories() {
Ok(items) => RuntimeList::new(items, Vec::new()),
Err(err) => RuntimeList::new(Vec::new(), vec![embedded_runtime_diagnostic(&err)]),
}
}
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
match self.runtime.working_directory(working_directory_id) {
Ok(working_directory) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(working_directory),
diagnostics: Vec::new(),
},
Err(err) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![embedded_runtime_diagnostic(&err)],
},
}
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> RuntimeWorkingDirectoryResult {
match self.runtime.cleanup_working_directory(working_directory_id) {
Ok(working_directory) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(working_directory),
diagnostics: Vec::new(),
},
Err(err) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![embedded_runtime_diagnostic(&err)],
},
}
}
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult { fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
if matches!( if matches!(
@ -1811,6 +1969,13 @@ impl RemoteWorkerRuntime {
self.send_json(self.http.post(self.endpoint(path)).json(body)) self.send_json(self.http.post(self.endpoint(path)).json(body))
} }
fn delete_json<T>(&self, path: &str) -> Result<T, RuntimeDiagnostic>
where
T: DeserializeOwned + Send + 'static,
{
self.send_json(self.http.delete(self.endpoint(path)))
}
fn send_json<T>(&self, request: RequestBuilder) -> Result<T, RuntimeDiagnostic> fn send_json<T>(&self, request: RequestBuilder) -> Result<T, RuntimeDiagnostic>
where where
T: DeserializeOwned + Send + 'static, T: DeserializeOwned + Send + 'static,
@ -2035,6 +2200,71 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
} }
} }
fn create_working_directory(
&self,
request: WorkingDirectoryRequest,
) -> RuntimeWorkingDirectoryResult {
match self.post_json::<_, RuntimeHttpWorkingDirectoryResponse>(
"/v1/working-directories",
&request,
) {
Ok(response) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(response.working_directory),
diagnostics: Vec::new(),
},
Err(diagnostic) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![diagnostic],
},
}
}
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
match self.get_json::<RuntimeHttpWorkingDirectoriesResponse>("/v1/working-directories") {
Ok(response) => RuntimeList::new(response.working_directories, Vec::new()),
Err(diagnostic) => RuntimeList::new(Vec::new(), vec![diagnostic]),
}
}
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
match self.get_json::<RuntimeHttpWorkingDirectoryResponse>(&format!(
"/v1/working-directories/{working_directory_id}"
)) {
Ok(response) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(response.working_directory),
diagnostics: Vec::new(),
},
Err(diagnostic) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![diagnostic],
},
}
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> RuntimeWorkingDirectoryResult {
match self.delete_json::<RuntimeHttpWorkingDirectoryResponse>(&format!(
"/v1/working-directories/{working_directory_id}"
)) {
Ok(response) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Accepted,
working_directory: Some(response.working_directory),
diagnostics: Vec::new(),
},
Err(diagnostic) => RuntimeWorkingDirectoryResult {
state: WorkerOperationState::Rejected,
working_directory: None,
diagnostics: vec![diagnostic],
},
}
}
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult { fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
if matches!( if matches!(
request.acceptance, request.acceptance,
@ -2801,6 +3031,14 @@ fn remote_reqwest_diagnostic(runtime_id: &str, err: reqwest::Error) -> RuntimeDi
} }
} }
fn sanitize_remote_runtime_message(code: &str, message: &str) -> String {
if message.contains('/') || message.contains('\\') {
format!("remote Runtime returned {code}; backend-private details were omitted")
} else {
message.to_string()
}
}
fn remote_http_status_diagnostic( fn remote_http_status_diagnostic(
runtime_id: &str, runtime_id: &str,
status: StatusCode, status: StatusCode,
@ -2811,6 +3049,10 @@ fn remote_http_status_diagnostic(
.as_ref() .as_ref()
.map(|error| error.error.code.as_str()) .map(|error| error.error.code.as_str())
.unwrap_or("remote_http_error"); .unwrap_or("remote_http_error");
let remote_message = error
.as_ref()
.map(|error| sanitize_remote_runtime_message(remote_code, &error.error.message))
.unwrap_or_else(|| "remote Runtime did not return a typed error body".to_string());
let (code, severity) = match status { let (code, severity) = match status {
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => { StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
("remote_runtime_auth_failed", DiagnosticSeverity::Error) ("remote_runtime_auth_failed", DiagnosticSeverity::Error)
@ -2820,14 +3062,12 @@ fn remote_http_status_diagnostic(
("remote_runtime_unsupported", DiagnosticSeverity::Warning) ("remote_runtime_unsupported", DiagnosticSeverity::Warning)
} }
_ if status.is_server_error() => ("remote_runtime_http_error", DiagnosticSeverity::Error), _ if status.is_server_error() => ("remote_runtime_http_error", DiagnosticSeverity::Error),
_ => ("remote_runtime_http_rejected", DiagnosticSeverity::Warning), _ => (remote_code, DiagnosticSeverity::Warning),
}; };
diagnostic( diagnostic(
code, code,
severity, severity,
format!( format!("Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {remote_message}"),
"Remote Runtime '{runtime_id}' rejected request ({remote_code}, HTTP {status}); internal details were sanitized"
),
) )
} }

View File

@ -14,9 +14,7 @@ use serde::{Deserialize, Serialize};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest}; use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use worker_runtime::working_directory::{ use worker_runtime::working_directory::LocalGitWorktreeMaterializer;
LocalGitWorktreeMaterializer, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
};
use crate::companion::{ use crate::companion::{
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse, CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
@ -67,6 +65,10 @@ use worker_runtime::interaction::{
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime"; const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
fn embedded_runtime_id() -> String {
EMBEDDED_WORKER_RUNTIME_ID.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthConfig { pub enum AuthConfig {
/// Local/dev-only mode. If a token is configured by a future entrypoint, it /// Local/dev-only mode. If a token is configured by a future entrypoint, it
@ -162,14 +164,10 @@ pub struct WorkspaceApi {
companion: Arc<CompanionConsole>, companion: Arc<CompanionConsole>,
observation_proxy: BackendObservationProxy, observation_proxy: BackendObservationProxy,
resource_broker: BackendResourceBroker, resource_broker: BackendResourceBroker,
working_directory_materializer: Arc<LocalGitWorktreeMaterializer>,
} }
impl WorkspaceApi { impl WorkspaceApi {
pub async fn new(config: ServerConfig, store: Arc<dyn ControlPlaneStore>) -> Result<Self> { pub async fn new(config: ServerConfig, store: Arc<dyn ControlPlaneStore>) -> Result<Self> {
let materializer = Arc::new(LocalGitWorktreeMaterializer::new(
config.embedded_runtime_store_root.clone(),
));
let resource_broker = BackendResourceBroker::default(); let resource_broker = BackendResourceBroker::default();
let execution_backend = WorkerRuntimeExecutionBackend::new( let execution_backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(config.workspace_root.clone()) ProfileRuntimeWorkerFactory::new(config.workspace_root.clone())
@ -180,7 +178,9 @@ impl WorkspaceApi {
"failed to initialize embedded Worker backend: {err}" "failed to initialize embedded Worker backend: {err}"
)) ))
})? })?
.with_working_directory_materializer((*materializer).clone()); .with_working_directory_materializer(LocalGitWorktreeMaterializer::new(
config.embedded_runtime_store_root.clone(),
));
Self::new_with_execution_backend_and_broker( Self::new_with_execution_backend_and_broker(
config, config,
store, store,
@ -241,9 +241,6 @@ impl WorkspaceApi {
let runtime = Arc::new(runtime); let runtime = Arc::new(runtime);
let companion = Arc::new(CompanionConsole::new(runtime.clone())); let companion = Arc::new(CompanionConsole::new(runtime.clone()));
let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone()); let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone());
let working_directory_materializer = Arc::new(LocalGitWorktreeMaterializer::new(
config.embedded_runtime_store_root.clone(),
));
Ok(Self { Ok(Self {
records: LocalProjectRecordReader::new(config.workspace_root.clone()), records: LocalProjectRecordReader::new(config.workspace_root.clone()),
config, config,
@ -252,7 +249,6 @@ impl WorkspaceApi {
companion, companion,
observation_proxy, observation_proxy,
resource_broker, resource_broker,
working_directory_materializer,
}) })
} }
@ -326,6 +322,14 @@ pub fn build_router(api: WorkspaceApi) -> Router {
) )
.route("/api/hosts", get(list_hosts)) .route("/api/hosts", get(list_hosts))
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts)) .route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories",
get(scoped_list_runtime_working_directories).post(scoped_create_runtime_working_directory),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}",
get(scoped_runtime_working_directory_detail).delete(scoped_cleanup_runtime_working_directory),
)
.route( .route(
"/api/w/{workspace_id}/working-directories", "/api/w/{workspace_id}/working-directories",
get(scoped_list_working_directories).post(scoped_create_working_directory), get(scoped_list_working_directories).post(scoped_create_working_directory),
@ -662,6 +666,8 @@ pub enum BrowserWorkingDirectoryCleanupPolicy {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct BrowserWorkingDirectoryCreateRequest { pub struct BrowserWorkingDirectoryCreateRequest {
#[serde(default = "embedded_runtime_id")]
pub runtime_id: String,
pub repository_id: String, pub repository_id: String,
#[serde(default)] #[serde(default)]
pub selector: Option<String>, pub selector: Option<String>,
@ -807,6 +813,13 @@ struct ScopedWorkingDirectoryPath {
working_directory_id: String, working_directory_id: String,
} }
#[derive(Debug, Deserialize)]
struct ScopedRuntimeWorkingDirectoryPath {
workspace_id: String,
runtime_id: String,
working_directory_id: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ScopedConfigBundlePath { struct ScopedConfigBundlePath {
workspace_id: String, workspace_id: String,
@ -1054,6 +1067,52 @@ async fn scoped_get_worker_launch_options(
get_worker_launch_options(State(api)).await get_worker_launch_options(State(api)).await
} }
async fn scoped_list_runtime_working_directories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let list = api
.runtime
.list_working_directories(&path.runtime_id)
.map_err(|err| err.into_error())?;
Ok(Json(BrowserWorkingDirectoryListResponse {
workspace_id: api.config.workspace_id.clone(),
items: list
.items
.into_iter()
.map(|status| status.summary)
.collect(),
diagnostics: list.diagnostics,
}))
}
async fn scoped_create_runtime_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(mut request): Json<BrowserWorkingDirectoryCreateRequest>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
request.runtime_id = path.runtime_id;
create_working_directory_for_runtime(api, request)
}
async fn scoped_runtime_working_directory_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
}
async fn scoped_cleanup_runtime_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
cleanup_working_directory_for_runtime(api, &path.runtime_id, &path.working_directory_id)
}
async fn scoped_list_working_directories( async fn scoped_list_working_directories(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>, AxumPath(path): AxumPath<ScopedWorkspacePath>,
@ -1073,22 +1132,7 @@ async fn scoped_create_working_directory(
Json(request): Json<BrowserWorkingDirectoryCreateRequest>, Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> { ) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let working_directory_request = working_directory_request_for_browser(&api, request)?; create_working_directory_for_runtime(api, request)
let binding = api
.working_directory_materializer
.create(&working_directory_request)
.map_err(|diagnostic| {
ApiError::from(Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: diagnostic.code,
message: diagnostic.message,
})
})?;
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: binding.status().summary,
diagnostics: Vec::new(),
}))
} }
async fn scoped_working_directory_detail( async fn scoped_working_directory_detail(
@ -1096,21 +1140,11 @@ async fn scoped_working_directory_detail(
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>, AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> { ) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let status = api working_directory_detail_for_runtime(
.working_directory_materializer api,
.working_directory_status(&path.working_directory_id) EMBEDDED_WORKER_RUNTIME_ID,
.map_err(|diagnostic| { &path.working_directory_id,
ApiError::from(Error::RuntimeOperationFailed { )
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: diagnostic.code,
message: diagnostic.message,
})
})?;
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: status.summary,
diagnostics: Vec::new(),
}))
} }
async fn scoped_cleanup_working_directory( async fn scoped_cleanup_working_directory(
@ -1118,20 +1152,89 @@ async fn scoped_cleanup_working_directory(
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>, AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> { ) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let status = api cleanup_working_directory_for_runtime(
.working_directory_materializer api,
.cleanup_working_directory(&path.working_directory_id) EMBEDDED_WORKER_RUNTIME_ID,
.map_err(|diagnostic| { &path.working_directory_id,
ApiError::from(Error::RuntimeOperationFailed { )
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), }
code: diagnostic.code,
message: diagnostic.message, fn create_working_directory_for_runtime(
}) api: WorkspaceApi,
})?; request: BrowserWorkingDirectoryCreateRequest,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
let runtime_id = request.runtime_id.clone();
let working_directory_request = working_directory_request_for_browser(&api, request)?;
let result = api
.runtime
.create_working_directory(&runtime_id, working_directory_request)
.map_err(|err| err.into_error())?;
let Some(working_directory) = result.working_directory else {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id,
code: "workspace_working_directory_create_failed".to_string(),
message: "Runtime did not create working directory".to_string(),
},
result.diagnostics,
));
};
Ok(Json(BrowserWorkingDirectoryDetailResponse { Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(), workspace_id: api.config.workspace_id.clone(),
item: status.summary, item: working_directory.summary,
diagnostics: Vec::new(), diagnostics: result.diagnostics,
}))
}
fn working_directory_detail_for_runtime(
api: WorkspaceApi,
runtime_id: &str,
working_directory_id: &str,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
let result = api
.runtime
.working_directory(runtime_id, working_directory_id)
.map_err(|err| err.into_error())?;
let Some(working_directory) = result.working_directory else {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_working_directory_lookup_failed".to_string(),
message: "Runtime did not return working directory".to_string(),
},
result.diagnostics,
));
};
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: working_directory.summary,
diagnostics: result.diagnostics,
}))
}
fn cleanup_working_directory_for_runtime(
api: WorkspaceApi,
runtime_id: &str,
working_directory_id: &str,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
let result = api
.runtime
.cleanup_working_directory(runtime_id, working_directory_id)
.map_err(|err| err.into_error())?;
let Some(working_directory) = result.working_directory else {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_working_directory_cleanup_failed".to_string(),
message: "Runtime did not cleanup working directory".to_string(),
},
result.diagnostics,
));
};
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: working_directory.summary,
diagnostics: result.diagnostics,
})) }))
} }
@ -1812,26 +1915,7 @@ async fn create_workspace_worker(
working_directory_id: selection.working_directory_id, working_directory_id: selection.working_directory_id,
relative_cwd: selection.relative_cwd, relative_cwd: selection.relative_cwd,
}); });
if resolved_working_directory.is_some() && request.runtime_id != EMBEDDED_WORKER_RUNTIME_ID { validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
return Err(Error::RuntimeOperationFailed {
runtime_id: request.runtime_id.clone(),
code: "working_directory_runtime_unsupported".to_string(),
message:
"created working directories are only supported by the embedded local runtime in v0"
.to_string(),
}
.into());
}
if let Some(working_directory) = resolved_working_directory.as_ref() {
api.working_directory_materializer
.bind_working_directory(
&working_directory.working_directory_id,
working_directory.relative_cwd.as_deref(),
)
.map_err(|diagnostic| {
working_directory_api_error(request.runtime_id.clone(), diagnostic)
})?;
}
let result = api let result = api
.runtime .runtime
.spawn_worker( .spawn_worker(
@ -2924,16 +3008,55 @@ fn working_directory_repository_options(
} }
fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult<Vec<WorkingDirectorySummary>> { fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult<Vec<WorkingDirectorySummary>> {
api.working_directory_materializer let list = api
.list_working_directories() .runtime
.map(|items| items.into_iter().map(|status| status.summary).collect()) .list_working_directories(EMBEDDED_WORKER_RUNTIME_ID)
.map_err(|diagnostic| { .map_err(|err| err.into_error())?;
ApiError::from(Error::RuntimeOperationFailed { if !list.diagnostics.is_empty() {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: diagnostic.code, code: "workspace_working_directory_list_failed".to_string(),
message: diagnostic.message, message: "Runtime did not list working directories".to_string(),
}) },
}) list.diagnostics,
));
}
Ok(list
.items
.into_iter()
.map(|status| status.summary)
.collect())
}
fn validate_working_directory_claim_for_browser(
claim: Option<&WorkingDirectoryClaim>,
) -> ApiResult<()> {
let Some(claim) = claim else {
return Ok(());
};
if let Some(relative_cwd) = claim.relative_cwd.as_deref() {
let path = Path::new(relative_cwd);
if path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, Component::CurDir | Component::Normal(_)))
{
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: "working_directory_relative_cwd_invalid".to_string(),
message: "working directory relative_cwd must stay inside the Runtime-owned working directory".to_string(),
},
vec![RuntimeDiagnostic {
code: "working_directory_relative_cwd_invalid".to_string(),
severity: DiagnosticSeverity::Error,
message: "relative_cwd must be a relative path without parent traversal".to_string(),
}],
));
}
}
Ok(())
} }
fn working_directory_request_for_browser( fn working_directory_request_for_browser(
@ -3065,24 +3188,6 @@ fn worker_create_not_accepted_error(
) )
} }
fn working_directory_api_error(
runtime_id: String,
diagnostic: WorkingDirectoryDiagnostic,
) -> ApiError {
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id,
code: diagnostic.code.clone(),
message: diagnostic.message.clone(),
},
vec![RuntimeDiagnostic {
code: diagnostic.code,
severity: DiagnosticSeverity::Error,
message: diagnostic.message,
}],
)
}
fn settings_bad_request(code: &'static str, message: &'static str) -> ApiError { fn settings_bad_request(code: &'static str, message: &'static str) -> ApiError {
Error::RuntimeOperationFailed { Error::RuntimeOperationFailed {
runtime_id: "workspace-backend".to_string(), runtime_id: "workspace-backend".to_string(),
@ -3475,6 +3580,7 @@ mod tests {
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
use tower::ServiceExt; use tower::ServiceExt;
use worker_runtime::resource::BackendResourceClient; use worker_runtime::resource::BackendResourceClient;
use worker_runtime::working_directory::WorkingDirectoryMaterializer;
use crate::hosts::{ use crate::hosts::{
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement, TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
@ -3731,7 +3837,6 @@ mod tests {
); );
} }
#[derive(Default)]
struct DeterministicExecutionBackend { struct DeterministicExecutionBackend {
contexts: std::sync::Mutex< contexts: std::sync::Mutex<
std::collections::HashMap< std::collections::HashMap<
@ -3739,6 +3844,26 @@ mod tests {
worker_runtime::execution::WorkerExecutionContext, worker_runtime::execution::WorkerExecutionContext,
>, >,
>, >,
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer,
}
impl Default for DeterministicExecutionBackend {
fn default() -> Self {
let unique = format!(
"yoi-deterministic-wd-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
Self {
contexts: std::sync::Mutex::new(std::collections::HashMap::new()),
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer::new(
std::env::temp_dir().join(unique),
),
}
}
} }
impl worker_runtime::execution::WorkerExecutionBackend for DeterministicExecutionBackend { impl worker_runtime::execution::WorkerExecutionBackend for DeterministicExecutionBackend {
@ -3746,10 +3871,65 @@ mod tests {
"deterministic-workspace-server-test" "deterministic-workspace-server-test"
} }
fn create_working_directory(
&self,
request: &worker_runtime::catalog::WorkingDirectoryRequest,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
Ok(self.materializer.create(request)?.status())
}
fn list_working_directories(&self) -> Vec<worker_runtime::catalog::WorkingDirectoryStatus> {
self.materializer
.list_working_directories()
.unwrap_or_default()
}
fn working_directory(
&self,
working_directory_id: &str,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.materializer
.working_directory_status(working_directory_id)
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.materializer
.cleanup_working_directory(working_directory_id)
}
fn spawn_worker( fn spawn_worker(
&self, &self,
request: worker_runtime::execution::WorkerExecutionSpawnRequest, request: worker_runtime::execution::WorkerExecutionSpawnRequest,
) -> worker_runtime::execution::WorkerExecutionSpawnResult { ) -> worker_runtime::execution::WorkerExecutionSpawnResult {
let working_directory = match request.request.working_directory.as_ref() {
Some(claim) => match self.materializer.bind_working_directory(
&claim.working_directory_id,
claim.relative_cwd.as_deref(),
) {
Ok(binding) => Some(binding.status()),
Err(diagnostic) => {
return worker_runtime::execution::WorkerExecutionSpawnResult::Rejected(
worker_runtime::execution::WorkerExecutionResult::rejected(
worker_runtime::execution::WorkerExecutionOperation::Spawn,
diagnostic.to_string(),
),
);
}
},
None => None,
};
self.contexts self.contexts
.lock() .lock()
.unwrap() .unwrap()
@ -3760,10 +3940,7 @@ mod tests {
self.backend_id(), self.backend_id(),
), ),
run_state: worker_runtime::execution::WorkerExecutionRunState::Idle, run_state: worker_runtime::execution::WorkerExecutionRunState::Idle,
working_directory: request working_directory,
.working_directory
.as_ref()
.map(|binding| binding.status()),
} }
} }

View File

@ -142,10 +142,12 @@
creatingWorkingDirectory = true; creatingWorkingDirectory = true;
submitError = null; submitError = null;
try { try {
const response = await fetch(workerApiPath('/working-directories'), { const response = await fetch(
workerApiPath(`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`), {
method: 'POST', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
runtime_id: runtimeId,
repository_id: workingDirectoryRepositoryId, repository_id: workingDirectoryRepositoryId,
selector: workingDirectorySelector || null, selector: workingDirectorySelector || null,
policy: { dirty_state: 'clean_point_only', cleanup: 'manual_or_worker_stop' }, policy: { dirty_state: 'clean_point_only', cleanup: 'manual_or_worker_stop' },

View File

@ -150,6 +150,7 @@ export type BrowserWorkerWorkingDirectorySelection = {
}; };
export type BrowserWorkingDirectoryCreateRequest = { export type BrowserWorkingDirectoryCreateRequest = {
runtime_id: string;
repository_id: string; repository_id: string;
selector?: string | null; selector?: string | null;
policy?: { policy?: {