refactor: remove working directory allocation terminology

This commit is contained in:
Keisuke Hirata 2026-07-08 04:56:22 +09:00
parent 5c33b2f63d
commit 25900f31cf
No known key found for this signature in database
12 changed files with 247 additions and 216 deletions

View File

@ -90,8 +90,8 @@ pub struct WorkingDirectoryRequest {
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryAllocationClaim { pub struct WorkingDirectoryClaim {
pub allocation_id: String, pub working_directory_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub relative_cwd: Option<String>, pub relative_cwd: Option<String>,
} }
@ -107,13 +107,13 @@ pub enum WorkingDirectoryStatusKind {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryCleanupTarget { pub struct WorkingDirectoryCleanupTarget {
pub kind: String, pub kind: String,
pub allocation_id: String, pub working_directory_id: String,
pub repository_id: String, pub repository_id: String,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectorySummary { pub struct WorkingDirectorySummary {
pub allocation_id: String, pub working_directory_id: String,
pub repository_id: String, pub repository_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_selector: Option<String>, pub requested_selector: Option<String>,
@ -152,9 +152,9 @@ pub struct CreateWorkerRequest {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_input: Option<WorkerInput>, pub initial_input: Option<WorkerInput>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option<WorkingDirectoryRequest>, pub working_directory_request: Option<WorkingDirectoryRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory_allocation: Option<WorkingDirectoryAllocationClaim>, pub working_directory: Option<WorkingDirectoryClaim>,
} }
/// Worker lifecycle status for the in-memory embedded runtime. /// Worker lifecycle status for the in-memory embedded runtime.

View File

@ -863,8 +863,8 @@ mod tests {
digest: bundle.metadata.digest, digest: bundle.metadata.digest,
}, },
initial_input: None, initial_input: None,
working_directory_request: None,
working_directory: None, working_directory: None,
working_directory_allocation: None,
} }
} }
@ -1148,8 +1148,8 @@ mod ws_tests {
digest: bundle.metadata.digest, digest: bundle.metadata.digest,
}, },
initial_input: None, initial_input: None,
working_directory_request: None,
working_directory: None, working_directory: None,
working_directory_allocation: None,
} }
} }

View File

@ -1592,8 +1592,8 @@ mod tests {
digest: bundle.metadata.digest, digest: bundle.metadata.digest,
}, },
initial_input: None, initial_input: None,
working_directory_request: None,
working_directory: None, working_directory: None,
working_directory_allocation: None,
} }
} }

View File

@ -375,23 +375,23 @@ where
let mut request = request; let mut request = request;
let working_directory = match ( let working_directory = match (
request.request.working_directory_request.as_ref(),
request.request.working_directory.as_ref(), request.request.working_directory.as_ref(),
request.request.working_directory_allocation.as_ref(),
) { ) {
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected( return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected(
WorkerExecutionOperation::Spawn, WorkerExecutionOperation::Spawn,
"worker spawn cannot specify both working_directory and working_directory_allocation", "worker spawn cannot specify both working_directory_request and working_directory",
)); ));
} }
(Some(workspace_request), None) => { (Some(working_directory_request), None) => {
let Some(materializer) = self.working_directory_materializer.as_ref() else { let Some(materializer) = self.working_directory_materializer.as_ref() else {
return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected( return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected(
WorkerExecutionOperation::Spawn, WorkerExecutionOperation::Spawn,
"working directory materialization requested, but no materializer is configured for this runtime backend", "working directory materialization requested, but no materializer is configured for this runtime backend",
)); ));
}; };
match materializer.materialize(&request.worker_ref, workspace_request) { match materializer.materialize(&request.worker_ref, working_directory_request) {
Ok(binding) => { Ok(binding) => {
request.working_directory = Some(binding.clone()); request.working_directory = Some(binding.clone());
Some(binding) Some(binding)
@ -406,16 +406,16 @@ where
} }
} }
} }
(None, Some(allocation)) => { (None, Some(working_directory)) => {
let Some(materializer) = self.working_directory_materializer.as_ref() else { let Some(materializer) = self.working_directory_materializer.as_ref() else {
return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected( return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::rejected(
WorkerExecutionOperation::Spawn, WorkerExecutionOperation::Spawn,
"working directory allocation requested, but no materializer is configured for this runtime backend", "working directory working_directory requested, but no materializer is configured for this runtime backend",
)); ));
}; };
match materializer.bind_allocation( match materializer.bind_working_directory(
&allocation.allocation_id, &working_directory.working_directory_id,
allocation.relative_cwd.as_deref(), working_directory.relative_cwd.as_deref(),
) { ) {
Ok(binding) => { Ok(binding) => {
request.working_directory = Some(binding.clone()); request.working_directory = Some(binding.clone());
@ -780,8 +780,8 @@ mod tests {
digest: bundle.metadata.digest, digest: bundle.metadata.digest,
}, },
initial_input: None, initial_input: None,
working_directory_request: None,
working_directory: None, working_directory: None,
working_directory_allocation: None,
} }
} }
@ -933,7 +933,7 @@ mod tests {
.unwrap(); .unwrap();
runtime.store_config_bundle(test_bundle()).unwrap(); runtime.store_config_bundle(test_bundle()).unwrap();
let mut request = create_request("chat"); let mut request = create_request("chat");
request.working_directory = Some(working_directory_request(repo.path())); request.working_directory_request = Some(working_directory_request(repo.path()));
let detail = runtime.create_worker(request).unwrap(); let detail = runtime.create_worker(request).unwrap();

View File

@ -12,7 +12,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
const WORKING_DIRECTORIES_DIR: &str = "working-directories"; const WORKING_DIRECTORIES_DIR: &str = "working-directories";
const MATERIALIZATION_RECORD: &str = "materialization.json"; const MATERIALIZATION_RECORD: &str = "materialization.json";
static NEXT_ALLOCATION_SEQUENCE: AtomicU64 = AtomicU64::new(0); static NEXT_WORKING_DIRECTORY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryEvidence { pub struct WorkingDirectoryEvidence {
@ -27,7 +27,7 @@ pub struct WorkingDirectoryEvidence {
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryAllocation { pub struct WorkingDirectory {
pub id: String, pub id: String,
pub repository_id: String, pub repository_id: String,
pub materializer_kind: MaterializerKind, pub materializer_kind: MaterializerKind,
@ -38,10 +38,10 @@ pub struct WorkingDirectoryAllocation {
pub status: WorkingDirectoryStatusKind, pub status: WorkingDirectoryStatusKind,
} }
impl WorkingDirectoryAllocation { impl WorkingDirectory {
pub fn status_summary(&self) -> WorkingDirectorySummary { pub fn status_summary(&self) -> WorkingDirectorySummary {
WorkingDirectorySummary { WorkingDirectorySummary {
allocation_id: self.id.clone(), working_directory_id: self.id.clone(),
repository_id: self.repository_id.clone(), repository_id: self.repository_id.clone(),
requested_selector: self.evidence.requested_selector.clone(), requested_selector: self.evidence.requested_selector.clone(),
materializer_kind: self.materializer_kind.clone(), materializer_kind: self.materializer_kind.clone(),
@ -57,10 +57,10 @@ impl WorkingDirectoryAllocation {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkingDirectoryBinding { pub struct WorkingDirectoryBinding {
pub allocation: WorkingDirectoryAllocation, pub working_directory: WorkingDirectory,
pub workspace_root: PathBuf, pub workspace_root: PathBuf,
pub cwd: PathBuf, pub cwd: PathBuf,
allocation_root: PathBuf, working_directory_root: PathBuf,
source_repository_path: PathBuf, source_repository_path: PathBuf,
} }
@ -73,8 +73,8 @@ impl WorkingDirectoryBinding {
&self.cwd &self.cwd
} }
pub fn allocation_root(&self) -> &Path { pub fn working_directory_root(&self) -> &Path {
&self.allocation_root &self.working_directory_root
} }
pub fn source_repository_path(&self) -> &Path { pub fn source_repository_path(&self) -> &Path {
@ -83,7 +83,7 @@ impl WorkingDirectoryBinding {
pub fn status(&self) -> WorkingDirectoryStatus { pub fn status(&self) -> WorkingDirectoryStatus {
WorkingDirectoryStatus { WorkingDirectoryStatus {
summary: self.allocation.status_summary(), summary: self.working_directory.status_summary(),
} }
} }
} }
@ -118,27 +118,29 @@ pub trait WorkingDirectoryMaterializer: Send + Sync + 'static {
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>; ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>;
fn preallocate( fn create(
&self, &self,
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>; ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>;
fn bind_allocation( fn bind_working_directory(
&self, &self,
allocation_id: &str, working_directory_id: &str,
relative_cwd: Option<&str>, relative_cwd: Option<&str>,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>; ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic>;
fn list_allocations(&self) -> Result<Vec<WorkingDirectoryStatus>, WorkingDirectoryDiagnostic>; fn list_working_directories(
fn allocation_status(
&self, &self,
allocation_id: &str, ) -> Result<Vec<WorkingDirectoryStatus>, WorkingDirectoryDiagnostic>;
fn working_directory_status(
&self,
working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic>; ) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic>;
fn cleanup_allocation( fn cleanup_working_directory(
&self, &self,
allocation_id: &str, working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic>; ) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic>;
fn cleanup(&self, binding: &WorkingDirectoryBinding) -> Result<(), WorkingDirectoryDiagnostic>; fn cleanup(&self, binding: &WorkingDirectoryBinding) -> Result<(), WorkingDirectoryDiagnostic>;
@ -160,7 +162,7 @@ impl LocalGitWorktreeMaterializer {
&self.runtime_root &self.runtime_root
} }
fn allocation_id(worker_ref: &WorkerRef, repository_id: &str) -> String { fn working_directory_id(worker_ref: &WorkerRef, repository_id: &str) -> String {
format!( format!(
"{}-{}-{}", "{}-{}-{}",
sanitize_path_component(worker_ref.runtime_id.as_str()), sanitize_path_component(worker_ref.runtime_id.as_str()),
@ -169,10 +171,10 @@ impl LocalGitWorktreeMaterializer {
) )
} }
fn allocation_root(&self, allocation_id: &str) -> PathBuf { fn working_directory_root(&self, working_directory_id: &str) -> PathBuf {
self.runtime_root self.runtime_root
.join(WORKING_DIRECTORIES_DIR) .join(WORKING_DIRECTORIES_DIR)
.join(allocation_id) .join(working_directory_id)
} }
fn write_record( fn write_record(
@ -180,11 +182,11 @@ impl LocalGitWorktreeMaterializer {
binding: &WorkingDirectoryBinding, binding: &WorkingDirectoryBinding,
) -> Result<(), WorkingDirectoryDiagnostic> { ) -> Result<(), WorkingDirectoryDiagnostic> {
let record = WorkingDirectoryMaterializationRecord { let record = WorkingDirectoryMaterializationRecord {
allocation: binding.allocation.clone(), working_directory: binding.working_directory.clone(),
workspace_root: binding.workspace_root.clone(), workspace_root: binding.workspace_root.clone(),
source_repository_path: binding.source_repository_path.clone(), source_repository_path: binding.source_repository_path.clone(),
}; };
let path = binding.allocation_root.join(MATERIALIZATION_RECORD); let path = binding.working_directory_root.join(MATERIALIZATION_RECORD);
let raw = serde_json::to_vec_pretty(&record).map_err(|error| { let raw = serde_json::to_vec_pretty(&record).map_err(|error| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_record_serialize_failed", "working_directory_record_serialize_failed",
@ -201,38 +203,38 @@ impl LocalGitWorktreeMaterializer {
fn read_binding( fn read_binding(
&self, &self,
allocation_id: &str, working_directory_id: &str,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
let allocation_root = self.allocation_root(allocation_id); let working_directory_root = self.working_directory_root(working_directory_id);
let path = allocation_root.join(MATERIALIZATION_RECORD); let path = working_directory_root.join(MATERIALIZATION_RECORD);
let raw = fs::read(&path).map_err(|_| { let raw = fs::read(&path).map_err(|_| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_allocation_not_found", "working_directory_not_found",
"working directory allocation was not found", "working directory working_directory was not found",
) )
})?; })?;
let record: WorkingDirectoryMaterializationRecord = serde_json::from_slice(&raw).map_err(|_| { let record: WorkingDirectoryMaterializationRecord = serde_json::from_slice(&raw).map_err(|_| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_record_invalid", "working_directory_record_invalid",
"working directory allocation record is invalid; backend-private path details were omitted", "working directory working_directory record is invalid; backend-private path details were omitted",
) )
})?; })?;
Ok(WorkingDirectoryBinding { Ok(WorkingDirectoryBinding {
allocation: record.allocation, working_directory: record.working_directory,
workspace_root: record.workspace_root.clone(), workspace_root: record.workspace_root.clone(),
cwd: record.workspace_root, cwd: record.workspace_root,
allocation_root, working_directory_root,
source_repository_path: record.source_repository_path, source_repository_path: record.source_repository_path,
}) })
} }
fn materialize_with_allocation_id( fn materialize_with_working_directory_id(
&self, &self,
allocation_id: String, working_directory_id: String,
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
cleanup_policy: &str, cleanup_policy: &str,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
validate_allocation_id(&allocation_id)?; validate_working_directory_id(&working_directory_id)?;
if request.materializer != MaterializerKind::LocalGitWorktree { if request.materializer != MaterializerKind::LocalGitWorktree {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_materializer_unsupported", "working_directory_materializer_unsupported",
@ -299,26 +301,26 @@ impl LocalGitWorktreeMaterializer {
.map(|value| value.trim().to_string()) .map(|value| value.trim().to_string())
.filter(|value| !value.is_empty()); .filter(|value| !value.is_empty());
let allocation_root = self.allocation_root(&allocation_id); let working_directory_root = self.working_directory_root(&working_directory_id);
let workspace_root = allocation_root let workspace_root = working_directory_root
.join("root") .join("root")
.join(sanitize_path_component(&request.repository.id)); .join(sanitize_path_component(&request.repository.id));
if workspace_root.exists() { if workspace_root.exists() {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_allocation_exists", "working_directory_exists",
"working directory allocation target already exists; cleanup or choose a new allocation", "working directory working_directory target already exists; cleanup or choose a new working_directory",
)); ));
} }
fs::create_dir_all(workspace_root.parent().ok_or_else(|| { fs::create_dir_all(workspace_root.parent().ok_or_else(|| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_invalid_target", "working_directory_invalid_target",
"working directory allocation target has no parent directory", "working directory working_directory target has no parent directory",
) )
})?) })?)
.map_err(|_| { .map_err(|_| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_create_failed", "working_directory_create_failed",
"failed to create working directory allocation directory; backend-private path details were omitted", "failed to create working directory working_directory directory; backend-private path details were omitted",
) )
})?; })?;
@ -334,8 +336,8 @@ impl LocalGitWorktreeMaterializer {
], ],
)?; )?;
let allocation = WorkingDirectoryAllocation { let working_directory = WorkingDirectory {
id: allocation_id.clone(), id: working_directory_id.clone(),
repository_id: request.repository.id.clone(), repository_id: request.repository.id.clone(),
materializer_kind: MaterializerKind::LocalGitWorktree, materializer_kind: MaterializerKind::LocalGitWorktree,
dirty_state_policy: DirtyStatePolicy::CleanPointOnly, dirty_state_policy: DirtyStatePolicy::CleanPointOnly,
@ -353,17 +355,17 @@ impl LocalGitWorktreeMaterializer {
}, },
cleanup_target: WorkingDirectoryCleanupTarget { cleanup_target: WorkingDirectoryCleanupTarget {
kind: "git_worktree".to_string(), kind: "git_worktree".to_string(),
allocation_id, working_directory_id,
repository_id: request.repository.id.clone(), repository_id: request.repository.id.clone(),
}, },
cleanup_policy: cleanup_policy.to_string(), cleanup_policy: cleanup_policy.to_string(),
status: WorkingDirectoryStatusKind::Active, status: WorkingDirectoryStatusKind::Active,
}; };
let binding = WorkingDirectoryBinding { let binding = WorkingDirectoryBinding {
allocation, working_directory,
workspace_root: workspace_root.clone(), workspace_root: workspace_root.clone(),
cwd: workspace_root, cwd: workspace_root,
allocation_root, working_directory_root,
source_repository_path: source_root, source_repository_path: source_root,
}; };
self.write_record(&binding)?; self.write_record(&binding)?;
@ -377,36 +379,46 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
worker_ref: &WorkerRef, worker_ref: &WorkerRef,
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
let allocation_id = Self::allocation_id(worker_ref, &request.repository.id); let working_directory_id = Self::working_directory_id(worker_ref, &request.repository.id);
self.materialize_with_allocation_id(allocation_id, request, "remove_on_worker_stop") self.materialize_with_working_directory_id(
working_directory_id,
request,
"remove_on_worker_stop",
)
} }
fn preallocate( fn create(
&self, &self,
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
let allocation_id = next_allocation_id(&request.repository.id); let working_directory_id = next_working_directory_id(&request.repository.id);
self.materialize_with_allocation_id(allocation_id, request, "manual_or_worker_stop") self.materialize_with_working_directory_id(
working_directory_id,
request,
"manual_or_worker_stop",
)
} }
fn bind_allocation( fn bind_working_directory(
&self, &self,
allocation_id: &str, working_directory_id: &str,
relative_cwd: Option<&str>, relative_cwd: Option<&str>,
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
validate_allocation_id(allocation_id)?; validate_working_directory_id(working_directory_id)?;
let binding = self.read_binding(allocation_id)?; let binding = self.read_binding(working_directory_id)?;
if binding.allocation.status != WorkingDirectoryStatusKind::Active { if binding.working_directory.status != WorkingDirectoryStatusKind::Active {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_not_active", "working_directory_not_active",
"working directory allocation is not active", "working directory working_directory is not active",
)); ));
} }
let cwd = validate_relative_cwd(binding.workspace_root(), relative_cwd)?; let cwd = validate_relative_cwd(binding.workspace_root(), relative_cwd)?;
Ok(WorkingDirectoryBinding { cwd, ..binding }) Ok(WorkingDirectoryBinding { cwd, ..binding })
} }
fn list_allocations(&self) -> Result<Vec<WorkingDirectoryStatus>, WorkingDirectoryDiagnostic> { fn list_working_directories(
&self,
) -> Result<Vec<WorkingDirectoryStatus>, WorkingDirectoryDiagnostic> {
let root = self.runtime_root.join(WORKING_DIRECTORIES_DIR); let root = self.runtime_root.join(WORKING_DIRECTORIES_DIR);
let entries = match fs::read_dir(&root) { let entries = match fs::read_dir(&root) {
Ok(entries) => entries, Ok(entries) => entries,
@ -414,7 +426,7 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
Err(_) => { Err(_) => {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_list_failed", "working_directory_list_failed",
"failed to list working directory allocations; backend-private path details were omitted", "failed to list working directory working_directories; backend-private path details were omitted",
)); ));
} }
}; };
@ -427,43 +439,46 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
if !file_type.is_dir() { if !file_type.is_dir() {
continue; continue;
} }
let allocation_id = entry.file_name().to_string_lossy().to_string(); let working_directory_id = entry.file_name().to_string_lossy().to_string();
if validate_allocation_id(&allocation_id).is_err() { if validate_working_directory_id(&working_directory_id).is_err() {
continue; continue;
} }
if let Ok(status) = self.allocation_status(&allocation_id) { if let Ok(status) = self.working_directory_status(&working_directory_id) {
statuses.push(status); statuses.push(status);
} }
} }
statuses statuses.sort_by(|left, right| {
.sort_by(|left, right| left.summary.allocation_id.cmp(&right.summary.allocation_id)); left.summary
.working_directory_id
.cmp(&right.summary.working_directory_id)
});
Ok(statuses) Ok(statuses)
} }
fn allocation_status( fn working_directory_status(
&self, &self,
allocation_id: &str, working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
validate_allocation_id(allocation_id)?; validate_working_directory_id(working_directory_id)?;
Ok(self.read_binding(allocation_id)?.status()) Ok(self.read_binding(working_directory_id)?.status())
} }
fn cleanup_allocation( fn cleanup_working_directory(
&self, &self,
allocation_id: &str, working_directory_id: &str,
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> { ) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
validate_allocation_id(allocation_id)?; validate_working_directory_id(working_directory_id)?;
let binding = self.read_binding(allocation_id)?; let binding = self.read_binding(working_directory_id)?;
self.cleanup(&binding)?; self.cleanup(&binding)?;
self.allocation_status(allocation_id) self.working_directory_status(working_directory_id)
} }
fn cleanup(&self, binding: &WorkingDirectoryBinding) -> Result<(), WorkingDirectoryDiagnostic> { fn cleanup(&self, binding: &WorkingDirectoryBinding) -> Result<(), WorkingDirectoryDiagnostic> {
let mut allocation = binding.allocation.clone(); let mut working_directory = binding.working_directory.clone();
let allocation_root = binding.allocation_root.canonicalize().map_err(|_| { let working_directory_root = binding.working_directory_root.canonicalize().map_err(|_| {
WorkingDirectoryDiagnostic::new( WorkingDirectoryDiagnostic::new(
"working_directory_cleanup_target_invalid", "working_directory_cleanup_target_invalid",
"working directory allocation root is unavailable; backend-private path details were omitted", "working directory working directory root is unavailable; backend-private path details were omitted",
) )
})?; })?;
let workspace_root = binding.workspace_root.canonicalize().map_err(|_| { let workspace_root = binding.workspace_root.canonicalize().map_err(|_| {
@ -472,10 +487,10 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
"working directory root is unavailable; backend-private path details were omitted", "working directory root is unavailable; backend-private path details were omitted",
) )
})?; })?;
if !workspace_root.starts_with(&allocation_root) { if !workspace_root.starts_with(&working_directory_root) {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_cleanup_escape_rejected", "working_directory_cleanup_escape_rejected",
"working directory cleanup target is outside the allocation root", "working directory cleanup target is outside the working directory root",
)); ));
} }
let workspace_root_arg = path_str(&workspace_root)?; let workspace_root_arg = path_str(&workspace_root)?;
@ -495,16 +510,16 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
Ok(()) Ok(())
} }
}); });
allocation.status = if remove_result.is_ok() { working_directory.status = if remove_result.is_ok() {
WorkingDirectoryStatusKind::Removed WorkingDirectoryStatusKind::Removed
} else { } else {
WorkingDirectoryStatusKind::CleanupPending WorkingDirectoryStatusKind::CleanupPending
}; };
let updated = WorkingDirectoryBinding { let updated = WorkingDirectoryBinding {
allocation, working_directory,
workspace_root: binding.workspace_root.clone(), workspace_root: binding.workspace_root.clone(),
cwd: binding.cwd.clone(), cwd: binding.cwd.clone(),
allocation_root: binding.allocation_root.clone(), working_directory_root: binding.working_directory_root.clone(),
source_repository_path: binding.source_repository_path.clone(), source_repository_path: binding.source_repository_path.clone(),
}; };
let _ = self.write_record(&updated); let _ = self.write_record(&updated);
@ -514,7 +529,7 @@ impl WorkingDirectoryMaterializer for LocalGitWorktreeMaterializer {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct WorkingDirectoryMaterializationRecord { struct WorkingDirectoryMaterializationRecord {
allocation: WorkingDirectoryAllocation, working_directory: WorkingDirectory,
workspace_root: PathBuf, workspace_root: PathBuf,
source_repository_path: PathBuf, source_repository_path: PathBuf,
} }
@ -586,29 +601,31 @@ fn sanitize_path_component(value: &str) -> String {
} }
} }
fn next_allocation_id(repository_id: &str) -> String { fn next_working_directory_id(repository_id: &str) -> String {
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis()) .map(|duration| duration.as_millis())
.unwrap_or_default(); .unwrap_or_default();
let sequence = NEXT_ALLOCATION_SEQUENCE.fetch_add(1, Ordering::Relaxed); let sequence = NEXT_WORKING_DIRECTORY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
format!( format!(
"alloc-{now}-{sequence}-{}", "alloc-{now}-{sequence}-{}",
sanitize_path_component(repository_id) sanitize_path_component(repository_id)
) )
} }
fn validate_allocation_id(allocation_id: &str) -> Result<(), WorkingDirectoryDiagnostic> { fn validate_working_directory_id(
let sanitized = sanitize_path_component(allocation_id); working_directory_id: &str,
if allocation_id.is_empty() ) -> Result<(), WorkingDirectoryDiagnostic> {
|| allocation_id != sanitized let sanitized = sanitize_path_component(working_directory_id);
|| allocation_id.contains(std::path::MAIN_SEPARATOR) if working_directory_id.is_empty()
|| allocation_id.contains('/') || working_directory_id != sanitized
|| allocation_id.contains('\\') || working_directory_id.contains(std::path::MAIN_SEPARATOR)
|| working_directory_id.contains('/')
|| working_directory_id.contains('\\')
{ {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_allocation_id_invalid", "working_directory_id_invalid",
"working directory allocation id is invalid", "working directory working_directory id is invalid",
)); ));
} }
Ok(()) Ok(())
@ -639,7 +656,7 @@ fn validate_relative_cwd(
{ {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_relative_cwd_invalid", "working_directory_relative_cwd_invalid",
"working directory relative_cwd must be a relative path inside the allocation root", "working directory relative_cwd must be a relative path inside the working directory root",
)); ));
} }
let target = workspace_root let target = workspace_root
@ -654,7 +671,7 @@ fn validate_relative_cwd(
if !target.starts_with(&workspace_root) || !target.is_dir() { if !target.starts_with(&workspace_root) || !target.is_dir() {
return Err(WorkingDirectoryDiagnostic::new( return Err(WorkingDirectoryDiagnostic::new(
"working_directory_relative_cwd_escape_rejected", "working_directory_relative_cwd_escape_rejected",
"working directory relative_cwd must resolve to a directory inside the allocation root", "working directory relative_cwd must resolve to a directory inside the working directory root",
)); ));
} }
Ok(target) Ok(target)
@ -728,16 +745,16 @@ mod tests {
"worktree should be detached, got {branch}" "worktree should be detached, got {branch}"
); );
assert_eq!( assert_eq!(
binding.allocation.materializer_kind, binding.working_directory.materializer_kind,
MaterializerKind::LocalGitWorktree MaterializerKind::LocalGitWorktree
); );
assert_eq!( assert_eq!(
binding.allocation.dirty_state_policy, binding.working_directory.dirty_state_policy,
DirtyStatePolicy::CleanPointOnly DirtyStatePolicy::CleanPointOnly
); );
assert!( assert!(
binding binding
.allocation_root() .working_directory_root()
.join(MATERIALIZATION_RECORD) .join(MATERIALIZATION_RECORD)
.exists() .exists()
); );
@ -805,7 +822,7 @@ mod tests {
} }
#[test] #[test]
fn preallocated_allocation_binds_safe_relative_cwd_and_lists_without_paths() { fn working_directory_binds_safe_relative_cwd_and_lists_without_paths() {
let repo = create_clean_repo(); let repo = create_clean_repo();
fs::create_dir_all(repo.path().join("crates/yoi")).unwrap(); fs::create_dir_all(repo.path().join("crates/yoi")).unwrap();
fs::write(repo.path().join("crates/yoi/lib.rs"), "// ok\n").unwrap(); fs::write(repo.path().join("crates/yoi/lib.rs"), "// ok\n").unwrap();
@ -813,15 +830,18 @@ mod tests {
git(repo.path(), &["commit", "-m", "add crate"]); git(repo.path(), &["commit", "-m", "add crate"]);
let runtime_root = tempfile::tempdir().unwrap(); let runtime_root = tempfile::tempdir().unwrap();
let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path()); let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path());
let allocation = materializer.preallocate(&request(repo.path())).unwrap(); let working_directory = materializer.create(&request(repo.path())).unwrap();
let bound = materializer let bound = materializer
.bind_allocation(&allocation.allocation.id, Some("crates/yoi")) .bind_working_directory(&working_directory.working_directory.id, Some("crates/yoi"))
.unwrap(); .unwrap();
assert_eq!(bound.cwd.file_name().unwrap(), "yoi"); assert_eq!(bound.cwd.file_name().unwrap(), "yoi");
let listed = materializer.list_allocations().unwrap(); let listed = materializer.list_working_directories().unwrap();
assert_eq!(listed.len(), 1); assert_eq!(listed.len(), 1);
assert_eq!(listed[0].summary.allocation_id, allocation.allocation.id); assert_eq!(
listed[0].summary.working_directory_id,
working_directory.working_directory.id
);
assert_eq!( assert_eq!(
listed[0].summary.requested_selector.as_deref(), listed[0].summary.requested_selector.as_deref(),
Some("HEAD") Some("HEAD")
@ -837,32 +857,35 @@ mod tests {
git(repo.path(), &["commit", "-m", "add inside"]); git(repo.path(), &["commit", "-m", "add inside"]);
let runtime_root = tempfile::tempdir().unwrap(); let runtime_root = tempfile::tempdir().unwrap();
let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path()); let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path());
let allocation = materializer.preallocate(&request(repo.path())).unwrap(); let working_directory = materializer.create(&request(repo.path())).unwrap();
assert_eq!( assert_eq!(
materializer materializer
.bind_allocation(&allocation.allocation.id, Some("/tmp")) .bind_working_directory(&working_directory.working_directory.id, Some("/tmp"))
.unwrap_err() .unwrap_err()
.code, .code,
"working_directory_relative_cwd_invalid" "working_directory_relative_cwd_invalid"
); );
assert_eq!( assert_eq!(
materializer materializer
.bind_allocation(&allocation.allocation.id, Some("../outside")) .bind_working_directory(&working_directory.working_directory.id, Some("../outside"))
.unwrap_err() .unwrap_err()
.code, .code,
"working_directory_relative_cwd_invalid" "working_directory_relative_cwd_invalid"
); );
assert_eq!( assert_eq!(
materializer materializer
.bind_allocation(&allocation.allocation.id, Some("missing")) .bind_working_directory(&working_directory.working_directory.id, Some("missing"))
.unwrap_err() .unwrap_err()
.code, .code,
"working_directory_relative_cwd_unavailable" "working_directory_relative_cwd_unavailable"
); );
assert_eq!( assert_eq!(
materializer materializer
.bind_allocation(&allocation.allocation.id, Some("inside/file.txt")) .bind_working_directory(
&working_directory.working_directory.id,
Some("inside/file.txt")
)
.unwrap_err() .unwrap_err()
.code, .code,
"working_directory_relative_cwd_escape_rejected" "working_directory_relative_cwd_escape_rejected"
@ -870,10 +893,11 @@ mod tests {
#[cfg(unix)] #[cfg(unix)]
{ {
std::os::unix::fs::symlink("/tmp", allocation.workspace_root().join("escape")).unwrap(); std::os::unix::fs::symlink("/tmp", working_directory.workspace_root().join("escape"))
.unwrap();
assert_eq!( assert_eq!(
materializer materializer
.bind_allocation(&allocation.allocation.id, Some("escape")) .bind_working_directory(&working_directory.working_directory.id, Some("escape"))
.unwrap_err() .unwrap_err()
.code, .code,
"working_directory_relative_cwd_escape_rejected" "working_directory_relative_cwd_escape_rejected"
@ -894,8 +918,12 @@ mod tests {
materializer.cleanup(&binding).unwrap(); materializer.cleanup(&binding).unwrap();
assert!(!workspace_root.exists()); assert!(!workspace_root.exists());
let raw = let raw = fs::read_to_string(
fs::read_to_string(binding.allocation_root().join(MATERIALIZATION_RECORD)).unwrap(); binding
.working_directory_root()
.join(MATERIALIZATION_RECORD),
)
.unwrap();
assert!(raw.contains("removed")); assert!(raw.contains("removed"));
} }
} }

View File

@ -387,9 +387,9 @@ fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState {
}, },
profile: Some(selector), profile: Some(selector),
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
}, },
); );

View File

@ -13,7 +13,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, WorkingDirectoryAllocationClaim, WorkingDirectoryRequest, WorkerStatus as EmbeddedWorkerStatus, WorkingDirectoryClaim, WorkingDirectoryRequest,
WorkingDirectorySummary, WorkingDirectorySummary,
}; };
use worker_runtime::config_bundle::{ use worker_runtime::config_bundle::{
@ -295,15 +295,15 @@ pub struct WorkerSpawnRequest {
pub profile: Option<ProfileSelector>, pub profile: Option<ProfileSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_input: Option<EmbeddedWorkerInput>, pub initial_input: Option<EmbeddedWorkerInput>,
/// Optional safe working-directory selector. The Workspace server resolves /// Optional safe working-directory creation request. The Workspace server resolves
/// this into a runtime-internal `WorkingDirectoryRequest` from configured /// this into a runtime-internal `WorkingDirectoryRequest` from configured
/// repositories before calling a host. /// repositories before calling a host.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option<WorkerSpawnWorkingDirectoryRequest>, pub working_directory_request: Option<WorkerSpawnWorkingDirectoryRequest>,
#[serde(skip, default)] #[serde(skip, default)]
pub resolved_working_directory: Option<WorkingDirectoryRequest>, pub resolved_working_directory_request: Option<WorkingDirectoryRequest>,
#[serde(skip, default)] #[serde(skip, default)]
pub resolved_working_directory_allocation: Option<WorkingDirectoryAllocationClaim>, pub resolved_working_directory: Option<WorkingDirectoryClaim>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -1356,8 +1356,8 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
profile, profile,
config_bundle, config_bundle,
initial_input: request.initial_input.clone(), initial_input: request.initial_input.clone(),
working_directory_request: request.resolved_working_directory_request.clone(),
working_directory: request.resolved_working_directory.clone(), working_directory: request.resolved_working_directory.clone(),
working_directory_allocation: request.resolved_working_directory_allocation.clone(),
}; };
match self.runtime.create_worker(create_request) { match self.runtime.create_worker(create_request) {
Ok(detail) => { Ok(detail) => {
@ -2032,8 +2032,8 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
profile, profile,
config_bundle, config_bundle,
initial_input: request.initial_input.clone(), initial_input: request.initial_input.clone(),
working_directory_request: request.resolved_working_directory_request.clone(),
working_directory: request.resolved_working_directory.clone(), working_directory: request.resolved_working_directory.clone(),
working_directory_allocation: request.resolved_working_directory_allocation.clone(),
}; };
match self.post_json::<_, RuntimeHttpWorkerResponse>("/v1/workers", &create) { match self.post_json::<_, RuntimeHttpWorkerResponse>("/v1/workers", &create) {
Ok(response) => WorkerSpawnResult { Ok(response) => WorkerSpawnResult {
@ -3191,9 +3191,9 @@ mod tests {
}, },
profile: None, profile: None,
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
} }
} }
@ -3323,9 +3323,9 @@ mod tests {
}, },
profile: None, profile: None,
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
}, },
) )
.unwrap(); .unwrap();
@ -3425,9 +3425,9 @@ mod tests {
}, },
profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())), profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())),
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
}, },
) )
.unwrap(); .unwrap();
@ -3456,9 +3456,9 @@ mod tests {
acceptance: WorkerSpawnAcceptanceRequirement::SocketReady, acceptance: WorkerSpawnAcceptanceRequirement::SocketReady,
profile: None, profile: None,
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
}, },
) )
.unwrap(); .unwrap();

View File

@ -46,7 +46,7 @@ use crate::store::{ControlPlaneStore, WorkspaceRecord};
use crate::{Error, Result}; use crate::{Error, Result};
use worker_runtime::catalog::{ use worker_runtime::catalog::{
ConfigBundleRef, DirtyStatePolicy, MaterializerKind, ProfileSelector, ConfigBundleRef, DirtyStatePolicy, MaterializerKind, ProfileSelector,
RepositorySelector as RuntimeRepositorySelector, WorkingDirectoryAllocationClaim, RepositorySelector as RuntimeRepositorySelector, WorkingDirectoryClaim,
WorkingDirectoryRepository, WorkingDirectoryRequest, WorkingDirectorySummary, WorkingDirectoryRepository, WorkingDirectoryRequest, WorkingDirectorySummary,
}; };
use worker_runtime::config_bundle::ConfigBundle; use worker_runtime::config_bundle::ConfigBundle;
@ -275,7 +275,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
get(scoped_list_working_directories).post(scoped_create_working_directory), get(scoped_list_working_directories).post(scoped_create_working_directory),
) )
.route( .route(
"/api/w/{workspace_id}/working-directories/{allocation_id}", "/api/w/{workspace_id}/working-directories/{working_directory_id}",
get(scoped_working_directory_detail).delete(scoped_cleanup_working_directory), get(scoped_working_directory_detail).delete(scoped_cleanup_working_directory),
) )
.route("/api/runtimes", get(list_runtimes)) .route("/api/runtimes", get(list_runtimes))
@ -626,7 +626,7 @@ pub struct BrowserWorkingDirectoryDetailResponse {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct BrowserWorkerWorkingDirectorySelection { pub struct BrowserWorkerWorkingDirectorySelection {
pub allocation_id: String, pub working_directory_id: String,
#[serde(default)] #[serde(default)]
pub relative_cwd: Option<String>, pub relative_cwd: Option<String>,
} }
@ -744,7 +744,7 @@ struct ScopedRuntimePath {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ScopedWorkingDirectoryPath { struct ScopedWorkingDirectoryPath {
workspace_id: String, workspace_id: String,
allocation_id: String, working_directory_id: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -903,10 +903,10 @@ 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 workspace_request = working_directory_request_for_browser(&api, request)?; let working_directory_request = working_directory_request_for_browser(&api, request)?;
let binding = api let binding = api
.working_directory_materializer .working_directory_materializer
.preallocate(&workspace_request) .create(&working_directory_request)
.map_err(|diagnostic| { .map_err(|diagnostic| {
ApiError::from(Error::RuntimeOperationFailed { ApiError::from(Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
@ -928,7 +928,7 @@ async fn scoped_working_directory_detail(
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let status = api let status = api
.working_directory_materializer .working_directory_materializer
.allocation_status(&path.allocation_id) .working_directory_status(&path.working_directory_id)
.map_err(|diagnostic| { .map_err(|diagnostic| {
ApiError::from(Error::RuntimeOperationFailed { ApiError::from(Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
@ -950,7 +950,7 @@ async fn scoped_cleanup_working_directory(
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let status = api let status = api
.working_directory_materializer .working_directory_materializer
.cleanup_allocation(&path.allocation_id) .cleanup_working_directory(&path.working_directory_id)
.map_err(|diagnostic| { .map_err(|diagnostic| {
ApiError::from(Error::RuntimeOperationFailed { ApiError::from(Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
@ -1621,28 +1621,28 @@ async fn create_workspace_worker(
content: initial_text, content: initial_text,
}) })
}; };
let resolved_working_directory_allocation = let resolved_working_directory =
request request
.working_directory .working_directory
.map(|selection| WorkingDirectoryAllocationClaim { .map(|selection| WorkingDirectoryClaim {
allocation_id: selection.allocation_id, working_directory_id: selection.working_directory_id,
relative_cwd: selection.relative_cwd, relative_cwd: selection.relative_cwd,
}); });
if resolved_working_directory_allocation.is_some() if resolved_working_directory.is_some() && request.runtime_id != EMBEDDED_WORKER_RUNTIME_ID {
&& request.runtime_id != EMBEDDED_WORKER_RUNTIME_ID
{
return Err(Error::RuntimeOperationFailed { return Err(Error::RuntimeOperationFailed {
runtime_id: request.runtime_id.clone(), runtime_id: request.runtime_id.clone(),
code: "working_directory_runtime_unsupported".to_string(), code: "working_directory_runtime_unsupported".to_string(),
message: "preallocated working directories are only supported by the embedded local runtime in v0".to_string(), message:
"created working directories are only supported by the embedded local runtime in v0"
.to_string(),
} }
.into()); .into());
} }
if let Some(allocation) = resolved_working_directory_allocation.as_ref() { if let Some(working_directory) = resolved_working_directory.as_ref() {
api.working_directory_materializer api.working_directory_materializer
.bind_allocation( .bind_working_directory(
&allocation.allocation_id, &working_directory.working_directory_id,
allocation.relative_cwd.as_deref(), working_directory.relative_cwd.as_deref(),
) )
.map_err(|diagnostic| { .map_err(|diagnostic| {
working_directory_api_error(request.runtime_id.clone(), diagnostic) working_directory_api_error(request.runtime_id.clone(), diagnostic)
@ -1660,9 +1660,9 @@ async fn create_workspace_worker(
}, },
profile: Some(profile_selector), profile: Some(profile_selector),
initial_input, initial_input,
working_directory: None, working_directory_request: None,
resolved_working_directory: None, resolved_working_directory_request: None,
resolved_working_directory_allocation, resolved_working_directory,
}, },
) )
.map_err(|err| err.into_error())?; .map_err(|err| err.into_error())?;
@ -1751,8 +1751,8 @@ async fn create_runtime_worker(
AxumPath(runtime_id): AxumPath<String>, AxumPath(runtime_id): AxumPath<String>,
Json(mut request): Json<WorkerSpawnRequest>, Json(mut request): Json<WorkerSpawnRequest>,
) -> ApiResult<Json<WorkerSpawnResult>> { ) -> ApiResult<Json<WorkerSpawnResult>> {
request.resolved_working_directory = request request.resolved_working_directory_request = request
.working_directory .working_directory_request
.as_ref() .as_ref()
.map(|working_directory| { .map(|working_directory| {
configured_working_directory_request(&api.config, working_directory) configured_working_directory_request(&api.config, working_directory)
@ -2713,7 +2713,7 @@ 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 api.working_directory_materializer
.list_allocations() .list_working_directories()
.map(|items| items.into_iter().map(|status| status.summary).collect()) .map(|items| items.into_iter().map(|status| status.summary).collect())
.map_err(|diagnostic| { .map_err(|diagnostic| {
ApiError::from(Error::RuntimeOperationFailed { ApiError::from(Error::RuntimeOperationFailed {
@ -3418,8 +3418,8 @@ mod tests {
digest: bundle.metadata.digest, digest: bundle.metadata.digest,
}, },
initial_input: None, initial_input: None,
working_directory_request: None,
working_directory: None, working_directory: None,
working_directory_allocation: None,
} }
} }
@ -3435,7 +3435,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn browser_working_directory_preallocate_list_detail_and_cleanup_are_path_safe() { async fn browser_working_directory_create_list_detail_and_cleanup_are_path_safe() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
init_clean_git_workspace(dir.path()); init_clean_git_workspace(dir.path());
let app = test_app(dir.path()).await; let app = test_app(dir.path()).await;
@ -3451,7 +3451,7 @@ mod tests {
}), }),
) )
.await; .await;
let allocation_id = created["item"]["allocation_id"] let working_directory_id = created["item"]["working_directory_id"]
.as_str() .as_str()
.unwrap() .unwrap()
.to_string(); .to_string();
@ -3463,11 +3463,14 @@ mod tests {
let list = get_json(app.clone(), &workspace_path).await; let list = get_json(app.clone(), &workspace_path).await;
assert_eq!(list["items"].as_array().unwrap().len(), 1); assert_eq!(list["items"].as_array().unwrap().len(), 1);
assert_eq!(list["items"][0]["allocation_id"], allocation_id); assert_eq!(
list["items"][0]["working_directory_id"],
working_directory_id
);
let detail_path = format!("{workspace_path}/{allocation_id}"); let detail_path = format!("{workspace_path}/{working_directory_id}");
let detail = get_json(app.clone(), &detail_path).await; let detail = get_json(app.clone(), &detail_path).await;
assert_eq!(detail["item"]["allocation_id"], allocation_id); assert_eq!(detail["item"]["working_directory_id"], working_directory_id);
let removed = request_json(app, "DELETE", &detail_path, None, StatusCode::OK).await; let removed = request_json(app, "DELETE", &detail_path, None, StatusCode::OK).await;
assert_eq!(removed["item"]["status"], "removed"); assert_eq!(removed["item"]["status"], "removed");
@ -3489,7 +3492,7 @@ mod tests {
}), }),
) )
.await; .await;
let allocation_id = created["item"]["allocation_id"].as_str().unwrap(); let working_directory_id = created["item"]["working_directory_id"].as_str().unwrap();
let response = request_json( let response = request_json(
app, app,
@ -3501,7 +3504,7 @@ mod tests {
"profile": "builtin:coder", "profile": "builtin:coder",
"initial_text": "", "initial_text": "",
"working_directory": { "working_directory": {
"allocation_id": allocation_id, "working_directory_id": working_directory_id,
"relative_cwd": "../escape" "relative_cwd": "../escape"
} }
})), })),
@ -3857,7 +3860,7 @@ mod tests {
"kind": "run_accepted", "kind": "run_accepted",
"expected_segments": 0 "expected_segments": 0
}, },
"working_directory": { "working_directory_request": {
"repository_id": TEST_REPOSITORY_ID, "repository_id": TEST_REPOSITORY_ID,
"local_path": dir.path().display().to_string() "local_path": dir.path().display().to_string()
} }
@ -3892,7 +3895,7 @@ mod tests {
"kind": "run_accepted", "kind": "run_accepted",
"expected_segments": 0 "expected_segments": 0
}, },
"working_directory": { "working_directory_request": {
"repository_id": TEST_REPOSITORY_ID, "repository_id": TEST_REPOSITORY_ID,
"selector": "HEAD" "selector": "HEAD"
} }
@ -4379,9 +4382,9 @@ mod tests {
}, },
profile: None, profile: None,
initial_input: None, initial_input: None,
working_directory: None, working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None, resolved_working_directory: None,
resolved_working_directory_allocation: None,
}, },
) )
.expect("spawn worker"); .expect("spawn worker");

View File

@ -48,7 +48,7 @@
let runtimeId = $state(''); let runtimeId = $state('');
let profile = $state('builtin:coder'); let profile = $state('builtin:coder');
let initialText = $state(''); let initialText = $state('');
let workingDirectoryAllocationId = $state(''); let workingDirectoryId = $state('');
let workingDirectoryRepositoryId = $state(''); let workingDirectoryRepositoryId = $state('');
let workingDirectorySelector = $state('HEAD'); let workingDirectorySelector = $state('HEAD');
let relativeCwd = $state(''); let relativeCwd = $state('');
@ -114,7 +114,7 @@
display_name: displayName, display_name: displayName,
profile, profile,
initial_text: initialText, initial_text: initialText,
working_directory_allocation_id: workingDirectoryAllocationId, working_directory_id: workingDirectoryId,
working_directory_repository_id: workingDirectoryRepositoryId, working_directory_repository_id: workingDirectoryRepositoryId,
working_directory_selector: workingDirectorySelector, working_directory_selector: workingDirectorySelector,
relative_cwd: relativeCwd, relative_cwd: relativeCwd,
@ -122,7 +122,7 @@
runtimeId = form.runtime_id; runtimeId = form.runtime_id;
displayName = form.display_name; displayName = form.display_name;
profile = form.profile; profile = form.profile;
workingDirectoryAllocationId = form.working_directory_allocation_id; workingDirectoryId = form.working_directory_id;
workingDirectoryRepositoryId = form.working_directory_repository_id; workingDirectoryRepositoryId = form.working_directory_repository_id;
workingDirectorySelector = form.working_directory_selector; workingDirectorySelector = form.working_directory_selector;
relativeCwd = form.relative_cwd; relativeCwd = form.relative_cwd;
@ -158,9 +158,9 @@
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse; const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
const items = options?.working_directories ?? []; const items = options?.working_directories ?? [];
options = options options = options
? { ...options, working_directories: [...items.filter((item) => item.allocation_id !== payload.item.allocation_id), payload.item] } ? { ...options, working_directories: [...items.filter((item) => item.working_directory_id !== payload.item.working_directory_id), payload.item] }
: options; : options;
workingDirectoryAllocationId = payload.item.allocation_id; workingDirectoryId = payload.item.working_directory_id;
} catch (err) { } catch (err) {
submitError = exceptionDisplayError(err, 'working directory create failed'); submitError = exceptionDisplayError(err, 'working directory create failed');
} finally { } finally {
@ -185,7 +185,7 @@
display_name: displayName, display_name: displayName,
profile, profile,
initial_text: initialText, initial_text: initialText,
working_directory_allocation_id: workingDirectoryAllocationId, working_directory_id: workingDirectoryId,
working_directory_repository_id: workingDirectoryRepositoryId, working_directory_repository_id: workingDirectoryRepositoryId,
working_directory_selector: workingDirectorySelector, working_directory_selector: workingDirectorySelector,
relative_cwd: relativeCwd, relative_cwd: relativeCwd,
@ -279,18 +279,18 @@
<fieldset class="worker-working-directory"> <fieldset class="worker-working-directory">
<legend>Working directory</legend> <legend>Working directory</legend>
<label> <label>
<span>Allocation</span> <span>Working directory</span>
<select bind:value={workingDirectoryAllocationId}> <select bind:value={workingDirectoryId}>
<option value="">No allocation selected</option> <option value="">No working directory selected</option>
{#each options?.working_directories ?? [] as directory} {#each options?.working_directories ?? [] as directory}
<option value={directory.allocation_id} disabled={directory.status !== 'active'}> <option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'} · {directory.resolved_commit.slice(0, 12)} · {directory.status} {directory.repository_id} · {directory.requested_selector ?? 'HEAD'} · {directory.resolved_commit.slice(0, 12)} · {directory.status}
</option> </option>
{/each} {/each}
</select> </select>
</label> </label>
<label> <label>
<span>Repository for new allocation</span> <span>Repository</span>
<select bind:value={workingDirectoryRepositoryId}> <select bind:value={workingDirectoryRepositoryId}>
{#if options?.repositories.length} {#if options?.repositories.length}
{#each options.repositories as repository} {#each options.repositories as repository}
@ -306,11 +306,11 @@
<input bind:value={workingDirectorySelector} autocomplete="off" placeholder="HEAD" /> <input bind:value={workingDirectorySelector} autocomplete="off" placeholder="HEAD" />
</label> </label>
<button type="button" disabled={creatingWorkingDirectory || !workingDirectoryRepositoryId} onclick={() => void createWorkingDirectory()}> <button type="button" disabled={creatingWorkingDirectory || !workingDirectoryRepositoryId} onclick={() => void createWorkingDirectory()}>
{creatingWorkingDirectory ? 'Allocating…' : 'Create working directory'} {creatingWorkingDirectory ? 'Creating…' : 'Create working directory'}
</button> </button>
<label> <label>
<span>Relative cwd</span> <span>Relative cwd</span>
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside allocation" /> <input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside working directory" />
</label> </label>
</fieldset> </fieldset>
<label> <label>
@ -335,7 +335,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<button type="submit" disabled={submitting || !runtimeId || !profile || !workingDirectoryAllocationId}> <button type="submit" disabled={submitting || !runtimeId || !profile || !workingDirectoryId}>
{submitting ? 'Starting…' : 'Start Coding Worker'} {submitting ? 'Starting…' : 'Start Coding Worker'}
</button> </button>
</form> </form>

View File

@ -116,7 +116,7 @@ export type WorkingDirectoryRepositoryOption = {
}; };
export type WorkingDirectorySummary = { export type WorkingDirectorySummary = {
allocation_id: string; working_directory_id: string;
repository_id: string; repository_id: string;
requested_selector?: string | null; requested_selector?: string | null;
materializer_kind: string; materializer_kind: string;
@ -127,7 +127,7 @@ export type WorkingDirectorySummary = {
cleanup_policy: string; cleanup_policy: string;
cleanup_target: { cleanup_target: {
kind: string; kind: string;
allocation_id: string; working_directory_id: string;
repository_id: string; repository_id: string;
}; };
}; };
@ -145,7 +145,7 @@ export type BrowserWorkingDirectoryListResponse = {
}; };
export type BrowserWorkerWorkingDirectorySelection = { export type BrowserWorkerWorkingDirectorySelection = {
allocation_id: string; working_directory_id: string;
relative_cwd?: string | null; relative_cwd?: string | null;
}; };

View File

@ -45,7 +45,7 @@ const options: WorkerLaunchOptionsResponse = {
], ],
working_directories: [ working_directories: [
{ {
allocation_id: "alloc-1-repo", working_directory_id: "wd-1-repo",
repository_id: "repo", repository_id: "repo",
requested_selector: "HEAD", requested_selector: "HEAD",
materializer_kind: "local_git_worktree", materializer_kind: "local_git_worktree",
@ -55,7 +55,7 @@ const options: WorkerLaunchOptionsResponse = {
cleanup_policy: "manual_or_worker_stop", cleanup_policy: "manual_or_worker_stop",
cleanup_target: { cleanup_target: {
kind: "git_worktree", kind: "git_worktree",
allocation_id: "alloc-1-repo", working_directory_id: "wd-1-repo",
repository_id: "repo", repository_id: "repo",
}, },
}, },
@ -69,7 +69,7 @@ Deno.test("defaultWorkerLaunchForm chooses active runtime, coder profile, reposi
display_name: "", display_name: "",
profile: "", profile: "",
initial_text: "hello", initial_text: "hello",
working_directory_allocation_id: "", working_directory_id: "",
working_directory_repository_id: "", working_directory_repository_id: "",
working_directory_selector: "", working_directory_selector: "",
relative_cwd: "", relative_cwd: "",
@ -79,18 +79,18 @@ Deno.test("defaultWorkerLaunchForm chooses active runtime, coder profile, reposi
assertEquals(form.display_name, "Coding Worker"); assertEquals(form.display_name, "Coding Worker");
assertEquals(form.profile, "builtin:coder"); assertEquals(form.profile, "builtin:coder");
assertEquals(form.initial_text, "hello"); assertEquals(form.initial_text, "hello");
assertEquals(form.working_directory_allocation_id, "alloc-1-repo"); assertEquals(form.working_directory_id, "wd-1-repo");
assertEquals(form.working_directory_repository_id, "repo"); assertEquals(form.working_directory_repository_id, "repo");
assertEquals(form.working_directory_selector, "HEAD"); assertEquals(form.working_directory_selector, "HEAD");
}); });
Deno.test("buildBrowserCreateWorkerRequest sends allocation id and relative cwd only", () => { Deno.test("buildBrowserCreateWorkerRequest sends working_directory id and relative cwd only", () => {
const request = buildBrowserCreateWorkerRequest({ const request = buildBrowserCreateWorkerRequest({
runtime_id: "embedded", runtime_id: "embedded",
display_name: "Worker", display_name: "Worker",
profile: "builtin:coder", profile: "builtin:coder",
initial_text: "go", initial_text: "go",
working_directory_allocation_id: "alloc-1-repo", working_directory_id: "wd-1-repo",
working_directory_repository_id: "repo", working_directory_repository_id: "repo",
working_directory_selector: "main", working_directory_selector: "main",
relative_cwd: "crates/yoi", relative_cwd: "crates/yoi",
@ -102,7 +102,7 @@ Deno.test("buildBrowserCreateWorkerRequest sends allocation id and relative cwd
profile: "builtin:coder", profile: "builtin:coder",
initial_text: "go", initial_text: "go",
working_directory: { working_directory: {
allocation_id: "alloc-1-repo", working_directory_id: "wd-1-repo",
relative_cwd: "crates/yoi", relative_cwd: "crates/yoi",
}, },
}); });

View File

@ -8,7 +8,7 @@ export type WorkerLaunchFormState = {
display_name: string; display_name: string;
profile: string; profile: string;
initial_text: string; initial_text: string;
working_directory_allocation_id: string; working_directory_id: string;
working_directory_repository_id: string; working_directory_repository_id: string;
working_directory_selector: string; working_directory_selector: string;
relative_cwd: string; relative_cwd: string;
@ -54,12 +54,12 @@ export function defaultWorkerLaunchForm(
? current.profile ? current.profile
: preferredProfile?.id || "", : preferredProfile?.id || "",
initial_text: current.initial_text, initial_text: current.initial_text,
working_directory_allocation_id: options?.working_directories.some( working_directory_id: options?.working_directories.some(
(directory) => (directory) =>
directory.allocation_id === current.working_directory_allocation_id, directory.working_directory_id === current.working_directory_id,
) )
? current.working_directory_allocation_id ? current.working_directory_id
: preferredWorkingDirectory?.allocation_id || "", : preferredWorkingDirectory?.working_directory_id || "",
working_directory_repository_id: current.working_directory_repository_id || working_directory_repository_id: current.working_directory_repository_id ||
preferredRepository?.id || "", preferredRepository?.id || "",
working_directory_selector: current.working_directory_selector || working_directory_selector: current.working_directory_selector ||
@ -77,9 +77,9 @@ export function buildBrowserCreateWorkerRequest(
profile: form.profile, profile: form.profile,
initial_text: form.initial_text, initial_text: form.initial_text,
}; };
if (form.working_directory_allocation_id) { if (form.working_directory_id) {
request.working_directory = { request.working_directory = {
allocation_id: form.working_directory_allocation_id, working_directory_id: form.working_directory_id,
}; };
const relativeCwd = form.relative_cwd.trim(); const relativeCwd = form.relative_cwd.trim();
if (relativeCwd) { if (relativeCwd) {