workdir: centralize workspace inventory contract

This commit is contained in:
2026-08-15 03:29:53 +09:00
parent 4cf34375a8
commit b910efd945
11 changed files with 449 additions and 218 deletions
Generated
+1
View File
@@ -539,6 +539,7 @@ dependencies = [
"tokio",
"tokio-tungstenite 0.29.0",
"uuid",
"workdir",
]
[[package]]
+1
View File
@@ -16,6 +16,7 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
tokio-tungstenite = { workspace = true }
uuid = { workspace = true }
workdir = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
+41 -40
View File
@@ -7,6 +7,7 @@ use std::fmt;
use tokio::sync::mpsc;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
pub use workdir::workspace::WorkingDirectorySummary as BackendWorkingDirectorySummary;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendRuntimeTarget {
@@ -98,46 +99,6 @@ pub struct BackendWorkerCapabilitySummary {
pub can_spawn_followup: bool,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkingDirectoryCleanupTarget {
pub kind: String,
pub working_directory_id: String,
pub repository_id: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkingDirectoryOccupancy {
pub runtime_id: String,
pub runtime_worker_id: u64,
pub worker_id: String,
pub display_name: String,
pub linked_at: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkingDirectorySummary {
pub working_directory_id: String,
pub repository_id: String,
#[serde(default)]
pub creation_selector: Option<String>,
#[serde(default)]
pub creation_ref: Option<String>,
#[serde(default)]
pub current_selector: Option<String>,
#[serde(default)]
pub current_ref: Option<String>,
pub materializer_kind: String,
#[serde(default)]
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
pub status: String,
#[serde(default)]
pub cleanliness: Option<String>,
#[serde(default)]
pub primary_worker_id: Option<u64>,
#[serde(default)]
pub occupied_by: Option<BackendWorkingDirectoryOccupancy>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerSummary {
pub runtime_id: String,
@@ -618,6 +579,46 @@ mod tests {
);
}
#[test]
fn backend_worker_summary_decodes_current_occupied_workdir_contract() {
let payload = serde_json::json!({
"runtime_id": "arcadia",
"worker_id": "worker-opaque-64",
"host_id": "host",
"display_name": "Coder",
"label": "Coder",
"workspace": {"visibility": "workspace", "identity": "workspace"},
"state": "idle",
"implementation": {"kind": "worker", "display_hint": "Coder"},
"capabilities": {"can_stop": true, "can_spawn_followup": false},
"working_directory": {
"working_directory_id": "wd-1",
"repository_id": "main",
"materializer_kind": "local_git_worktree",
"status": "active",
"occupied_by": {
"runtime_id": "arcadia",
"worker_id": "worker-opaque-64",
"display_name": "Coder",
"linked_at": "2026-08-12T00:00:00Z"
}
}
});
let worker: BackendWorkerSummary = serde_json::from_value(payload.clone()).unwrap();
let occupied_by = worker
.working_directory
.unwrap()
.occupied_by
.expect("occupied Workdir");
assert_eq!(occupied_by.worker.runtime_id, "arcadia");
assert_eq!(occupied_by.worker.worker_id, "worker-opaque-64");
let mut stale = payload;
stale["working_directory"]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
assert!(serde_json::from_value::<BackendWorkerSummary>(stale).is_err());
}
#[test]
fn workers_path_can_be_workspace_scoped_for_status_queries() {
let path = backend_runtime_workers_path(Some("team main"), "runtime/one");
+1
View File
@@ -8,6 +8,7 @@
pub mod http;
mod local;
mod operation;
pub mod workspace;
use std::path::{Path, PathBuf};
use std::sync::Arc;
+280
View File
@@ -0,0 +1,280 @@
//! Shared Workspace-facing Workdir inventory transport contracts.
//!
//! These projections describe Workspace inventory and durable occupancy. They
//! intentionally do not expose provider/session handles, host paths, Runtime
//! URLs, or credentials. Runtime-local Workdir operation contracts live in
//! [`crate::http`].
use serde::{Deserialize, Serialize};
/// Stable Workspace identity for a Worker hosted by a Runtime.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RuntimeWorkerRef {
pub runtime_id: String,
/// Runtime-owned opaque Worker id. Consumers must not assume a numeric id.
pub worker_id: String,
}
impl RuntimeWorkerRef {
pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
Self {
runtime_id: runtime_id.into(),
worker_id: worker_id.into(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaterializerKind {
#[default]
LocalGitWorktree,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryStatusKind {
Active,
CleanupPending,
Corrupted,
NotFound,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCleanupTarget {
pub kind: String,
pub working_directory_id: String,
pub repository_id: String,
}
/// Durable Workspace occupancy projection for one Workdir.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct WorkingDirectoryOccupancy {
#[serde(flatten)]
pub worker: RuntimeWorkerRef,
pub display_name: String,
pub linked_at: String,
}
impl<'de> Deserialize<'de> for WorkingDirectoryOccupancy {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
runtime_id: String,
worker_id: String,
display_name: String,
linked_at: String,
}
let wire = Wire::deserialize(deserializer)?;
Ok(Self {
worker: RuntimeWorkerRef::new(wire.runtime_id, wire.worker_id),
display_name: wire.display_name,
linked_at: wire.linked_at,
})
}
}
/// Immutable materialization provenance retained by Workspace inventory.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_ref: Option<String>,
pub materializer_kind: MaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
}
/// Latest provider-neutral observation attached to Workspace inventory.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCurrentObservation {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_ref: Option<String>,
pub status: WorkingDirectoryStatusKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanliness: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub primary_worker_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub occupied_by: Option<WorkingDirectoryOccupancy>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectorySummary {
pub working_directory_id: String,
pub repository_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_ref: Option<String>,
pub materializer_kind: MaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
pub status: WorkingDirectoryStatusKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanliness: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub primary_worker_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub occupied_by: Option<WorkingDirectoryOccupancy>,
}
impl WorkingDirectorySummary {
/// Workspace-managed inventory rows carry explicit cleanup authority.
pub fn is_workspace_managed(&self) -> bool {
self.cleanup_target.is_some()
}
pub fn provenance(&self) -> WorkingDirectoryProvenance {
WorkingDirectoryProvenance {
creation_selector: self.creation_selector.clone(),
creation_ref: self.creation_ref.clone(),
materializer_kind: self.materializer_kind.clone(),
cleanup_target: self.cleanup_target.clone(),
}
}
pub fn current_observation(&self) -> WorkingDirectoryCurrentObservation {
WorkingDirectoryCurrentObservation {
current_selector: self.current_selector.clone(),
current_ref: self.current_ref.clone(),
status: self.status.clone(),
cleanliness: self.cleanliness.clone(),
primary_worker_id: self.primary_worker_id,
occupied_by: self.occupied_by.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryDiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryDiagnostic {
pub code: String,
pub severity: WorkingDirectoryDiagnosticSeverity,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryListResponse {
pub workspace_id: String,
pub items: Vec<WorkingDirectorySummary>,
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryDetailResponse {
pub workspace_id: String,
pub item: WorkingDirectorySummary,
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn occupied_and_free_list_response_round_trips() {
let response = WorkingDirectoryListResponse {
workspace_id: "workspace".to_string(),
items: vec![
WorkingDirectorySummary {
working_directory_id: "occupied".to_string(),
repository_id: "repo".to_string(),
creation_selector: Some("develop".to_string()),
creation_ref: Some("abc123".to_string()),
current_selector: Some("work/ticket".to_string()),
current_ref: Some("def456".to_string()),
materializer_kind: MaterializerKind::LocalGitWorktree,
cleanup_target: Some(WorkingDirectoryCleanupTarget {
kind: "git_worktree".to_string(),
working_directory_id: "occupied".to_string(),
repository_id: "repo".to_string(),
}),
status: WorkingDirectoryStatusKind::Active,
cleanliness: Some("clean".to_string()),
primary_worker_id: None,
occupied_by: Some(WorkingDirectoryOccupancy {
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
display_name: "Coder".to_string(),
linked_at: "2026-08-12T00:00:00Z".to_string(),
}),
},
WorkingDirectorySummary {
working_directory_id: "free".to_string(),
repository_id: "repo".to_string(),
creation_selector: None,
creation_ref: None,
current_selector: None,
current_ref: Some("987fed".to_string()),
materializer_kind: MaterializerKind::LocalGitWorktree,
cleanup_target: None,
status: WorkingDirectoryStatusKind::Active,
cleanliness: Some("unknown".to_string()),
primary_worker_id: None,
occupied_by: None,
},
],
diagnostics: vec![WorkingDirectoryDiagnostic {
code: "observed".to_string(),
severity: WorkingDirectoryDiagnosticSeverity::Info,
message: "inventory observed".to_string(),
}],
};
let encoded = serde_json::to_value(&response).unwrap();
assert_eq!(
encoded["items"][0]["occupied_by"]["worker_id"],
"worker-opaque-64"
);
assert!(
encoded["items"][0]["occupied_by"]
.get("runtime_worker_id")
.is_none()
);
assert!(encoded["items"][1].get("occupied_by").is_none());
let mut stale = encoded.clone();
stale["items"][0]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
assert!(serde_json::from_value::<WorkingDirectoryListResponse>(stale).is_err());
let decoded: WorkingDirectoryListResponse = serde_json::from_value(encoded).unwrap();
assert_eq!(decoded, response);
let detail = WorkingDirectoryDetailResponse {
workspace_id: decoded.workspace_id.clone(),
item: decoded.items[0].clone(),
diagnostics: decoded.diagnostics.clone(),
};
let encoded = serde_json::to_value(&detail).unwrap();
let decoded: WorkingDirectoryDetailResponse = serde_json::from_value(encoded).unwrap();
assert_eq!(decoded, detail);
}
}
+5 -59
View File
@@ -92,12 +92,11 @@ pub struct WorkingDirectoryRepository {
pub selector: Option<RepositorySelector>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaterializerKind {
#[default]
LocalGitWorktree,
}
pub use workdir::workspace::{
MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryCurrentObservation,
WorkingDirectoryOccupancy, WorkingDirectoryProvenance, WorkingDirectoryStatusKind,
WorkingDirectorySummary,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryRequest {
@@ -117,59 +116,6 @@ pub struct WorkingDirectoryClaim {
pub relative_cwd: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryStatusKind {
Active,
CleanupPending,
Corrupted,
NotFound,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryCleanupTarget {
pub kind: String,
pub working_directory_id: String,
pub repository_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryOccupancy {
#[serde(flatten)]
pub worker: RuntimeWorkerRef,
pub display_name: String,
pub linked_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectorySummary {
pub working_directory_id: String,
pub repository_id: String,
/// Selector used to create this Workdir, retained as immutable materialization evidence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_selector: Option<String>,
/// Provider-specific immutable ref resolved when this Workdir was created.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_ref: Option<String>,
/// Selector currently observed from the materialized Workdir, when one exists.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_selector: Option<String>,
/// Provider-specific immutable ref currently observed from the materialized Workdir.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_ref: Option<String>,
pub materializer_kind: MaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
pub status: WorkingDirectoryStatusKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanliness: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub primary_worker_id: Option<WorkerId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub occupied_by: Option<WorkingDirectoryOccupancy>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryStatus {
pub summary: WorkingDirectorySummary,
+10 -22
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use std::fmt;
pub use workdir::workspace::RuntimeWorkerRef;
/// Runtime-local Worker identity.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
@@ -30,29 +31,16 @@ impl fmt::Display for WorkerId {
}
}
/// Backend-visible Worker identity, namespaced by the Runtime that owns the Worker record.
///
/// This is intentionally distinct from [`WorkerRef`], which is meaningful only inside one
/// Runtime. Do not flatten this reference into a concatenated string for authority decisions.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RuntimeWorkerRef {
pub runtime_id: String,
pub worker_id: String,
}
/// Convert an opaque Workspace Worker reference only at the Runtime-local boundary.
impl TryFrom<&RuntimeWorkerRef> for WorkerRef {
type Error = std::num::ParseIntError;
impl RuntimeWorkerRef {
pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
Self {
runtime_id: runtime_id.into(),
worker_id: worker_id.into(),
}
}
pub fn local_worker_ref(&self) -> Result<WorkerRef, std::num::ParseIntError> {
self.worker_id
fn try_from(value: &RuntimeWorkerRef) -> Result<Self, Self::Error> {
value
.worker_id
.parse::<u64>()
.map(WorkerId::new)
.map(WorkerRef::new)
.map(Self::new)
}
}
@@ -78,7 +66,7 @@ mod tests {
assert_eq!(worker.runtime_id, "arcadia");
assert_eq!(worker.worker_id, "30");
assert_eq!(
worker.local_worker_ref().unwrap(),
WorkerRef::try_from(&worker).unwrap(),
WorkerRef::new(WorkerId::new(30))
);
assert_eq!(
@@ -90,6 +78,6 @@ mod tests {
#[test]
fn runtime_worker_ref_does_not_treat_composite_text_as_local_worker_id() {
let worker = RuntimeWorkerRef::new("arcadia", "embedded-worker-runtime-5");
assert!(worker.local_worker_ref().is_err());
assert!(WorkerRef::try_from(&worker).is_err());
}
}
+3 -2
View File
@@ -467,8 +467,9 @@ impl Runtime {
mut status: CatalogWorkingDirectoryStatus,
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
let state = self.lock()?;
status.summary.primary_worker_id =
state.primary_worker_id_for_workdir(status.summary.working_directory_id.as_str());
status.summary.primary_worker_id = state
.primary_worker_id_for_workdir(status.summary.working_directory_id.as_str())
.map(|worker_id| worker_id.as_u64());
Ok(status)
}
+3 -4
View File
@@ -154,7 +154,7 @@ impl WorkerObservationProvider for RuntimeGrantedWorkerObservationProvider {
if grant.runtime_id != self.runtime_id {
continue;
}
let Ok(worker_ref) = grant.local_worker_ref() else {
let Ok(worker_ref) = WorkerRef::try_from(grant) else {
continue;
};
let Some((workspace_id, state, _)) = self.hub.get(&worker_ref) else {
@@ -192,9 +192,8 @@ impl WorkerObservationProvider for RuntimeGrantedWorkerObservationProvider {
if !self.grants.contains(&grant) || runtime_id != &self.runtime_id {
return Err(WorkerObservationError::NotFound);
}
let worker_ref = grant
.local_worker_ref()
.map_err(|_| WorkerObservationError::NotFound)?;
let worker_ref =
WorkerRef::try_from(&grant).map_err(|_| WorkerObservationError::NotFound)?;
let (workspace_id, _, sink) = self
.hub
.get(&worker_ref)
@@ -14,6 +14,10 @@ use llm_engine::tool::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
use workdir::workspace::{
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
WorkingDirectoryListResponse as WorkdirListResponse,
};
use workdir::{
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
@@ -644,71 +648,6 @@ struct WorkdirDeleteInput {
working_directory_id: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirListResponse {
workspace_id: String,
items: Vec<WorkdirSummary>,
diagnostics: Vec<WorkdirDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirDetailResponse {
workspace_id: String,
item: WorkdirSummary,
diagnostics: Vec<WorkdirDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirSummary {
working_directory_id: String,
repository_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
creation_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
creation_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
current_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
current_ref: Option<String>,
materializer_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cleanup_target: Option<WorkdirCleanupTarget>,
status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cleanliness: Option<String>,
#[serde(
default,
alias = "primary_worker_id",
skip_serializing_if = "Option::is_none"
)]
attached_worker_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
occupied_by: Option<WorkdirOccupancy>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirCleanupTarget {
kind: String,
working_directory_id: String,
repository_id: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirOccupancy {
runtime_id: String,
runtime_worker_id: u64,
worker_id: String,
display_name: String,
linked_at: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirDiagnostic {
code: String,
severity: String,
message: String,
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
@@ -783,7 +722,13 @@ mod tests {
"repository_id": "main"
},
"status": "active",
"cleanliness": "clean"
"cleanliness": "clean",
"occupied_by": {
"runtime_id": "arcadia",
"worker_id": "worker-opaque-64",
"display_name": "Coder",
"linked_at": "2026-08-12T00:00:00Z"
}
})
}
@@ -889,6 +834,18 @@ mod tests {
assert_eq!(delete_schema()["required"], json!(["working_directory_id"]));
}
#[test]
fn legacy_runtime_worker_id_is_rejected_by_workspace_workdir_contract() {
let mut response = json!({
"workspace_id": "workspace/one",
"items": [workdir_json("wd-1")],
"diagnostics": []
});
response["items"][0]["occupied_by"]["runtime_worker_id"] = json!(64);
assert!(serde_json::from_value::<WorkdirListResponse>(response).is_err());
}
#[test]
fn list_create_explicit_attach_detach_and_delete_use_scoped_workspace_authority_paths() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![
@@ -925,7 +882,18 @@ mod tests {
]));
let backend = WorkspaceHttpWorkdirBackend::new(client.clone());
backend.list().unwrap();
let listed = backend.list().unwrap();
let listed: serde_json::Value =
serde_json::from_str(listed.content.as_deref().unwrap()).unwrap();
assert_eq!(
listed["items"][0]["occupied_by"]["worker_id"],
"worker-opaque-64"
);
assert!(
listed["items"][0]["occupied_by"]
.get("runtime_worker_id")
.is_none()
);
let created = backend
.create(WorkdirCreateInput {
runtime_id: "runtime/one".to_string(),
+69 -24
View File
@@ -44,6 +44,13 @@ use webauthn_rs::prelude::{
};
use workdir::WorkdirSessionHandle;
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
use workdir::workspace::{
MaterializerKind, WorkingDirectoryCleanupTarget,
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
WorkingDirectoryDiagnostic, WorkingDirectoryDiagnosticSeverity,
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryOccupancy,
WorkingDirectoryStatusKind, WorkingDirectorySummary,
};
use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjectRef};
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
@@ -101,10 +108,8 @@ use crate::store::{
};
use crate::{Error, Result};
use worker_runtime::catalog::{
ConfigBundleRef, MaterializerKind, ProfileSelector,
RepositorySelector as RuntimeRepositorySelector, WorkingDirectoryClaim,
WorkingDirectoryOccupancy, WorkingDirectoryRepository, WorkingDirectoryRequest,
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceApiRef,
ConfigBundleRef, ProfileSelector, RepositorySelector as RuntimeRepositorySelector,
WorkingDirectoryClaim, WorkingDirectoryRepository, WorkingDirectoryRequest, WorkspaceApiRef,
};
use worker_runtime::config_bundle::ConfigBundle;
use worker_runtime::http_server::{
@@ -2027,20 +2032,6 @@ pub struct BrowserWorkingDirectoryCreateRequest {
pub selector: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkingDirectoryListResponse {
pub workspace_id: String,
pub items: Vec<WorkingDirectorySummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkingDirectoryDetailResponse {
pub workspace_id: String,
pub item: WorkingDirectorySummary,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkerWorkingDirectorySelection {
@@ -5859,6 +5850,23 @@ async fn scoped_get_worker_launch_options(
get_worker_launch_options(State(api)).await
}
fn working_directory_diagnostics(
diagnostics: Vec<RuntimeDiagnostic>,
) -> Vec<WorkingDirectoryDiagnostic> {
diagnostics
.into_iter()
.map(|diagnostic| WorkingDirectoryDiagnostic {
code: diagnostic.code,
severity: match diagnostic.severity {
DiagnosticSeverity::Info => WorkingDirectoryDiagnosticSeverity::Info,
DiagnosticSeverity::Warning => WorkingDirectoryDiagnosticSeverity::Warning,
DiagnosticSeverity::Error => WorkingDirectoryDiagnosticSeverity::Error,
},
message: diagnostic.message,
})
.collect()
}
async fn scoped_list_runtime_working_directories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
@@ -5868,7 +5876,7 @@ async fn scoped_list_runtime_working_directories(
Ok(Json(BrowserWorkingDirectoryListResponse {
workspace_id: api.config.workspace_id.clone(),
items,
diagnostics,
diagnostics: working_directory_diagnostics(diagnostics),
}))
}
@@ -6020,7 +6028,7 @@ fn create_working_directory_for_runtime(
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
diagnostics: working_directory_diagnostics(result.diagnostics),
}))
}
@@ -6041,7 +6049,7 @@ fn working_directory_detail_for_runtime(
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
diagnostics: working_directory_diagnostics(result.diagnostics),
}));
}
if let Some(record) = api
@@ -6051,7 +6059,7 @@ fn working_directory_detail_for_runtime(
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: projected_workdir_summary_from_record(&api, &record)?,
diagnostics: result.diagnostics,
diagnostics: working_directory_diagnostics(result.diagnostics),
}));
}
Err(ApiError::with_diagnostics(
@@ -6110,7 +6118,7 @@ fn cleanup_working_directory_for_runtime(
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
diagnostics: working_directory_diagnostics(result.diagnostics),
}))
}
@@ -10859,7 +10867,7 @@ fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirecto
current_selector: record.current_selector.clone(),
current_ref: record.current_ref.clone(),
materializer_kind: MaterializerKind::LocalGitWorktree,
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
cleanup_target: Some(WorkingDirectoryCleanupTarget {
kind: "local_git_worktree".to_string(),
working_directory_id: record.workdir_id.clone(),
repository_id: record.repository_id.clone(),
@@ -17412,6 +17420,43 @@ mod tests {
assert_eq!(mutation.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[test]
fn workspace_workdir_response_serializes_shared_occupied_contract() {
let response = BrowserWorkingDirectoryListResponse {
workspace_id: TEST_WORKSPACE_ID.to_string(),
items: vec![WorkingDirectorySummary {
working_directory_id: "wd-1".to_string(),
repository_id: "main".to_string(),
creation_selector: None,
creation_ref: None,
current_selector: Some("work/ticket".to_string()),
current_ref: Some("abc123".to_string()),
materializer_kind: MaterializerKind::LocalGitWorktree,
cleanup_target: None,
status: WorkingDirectoryStatusKind::Active,
cleanliness: Some("clean".to_string()),
primary_worker_id: None,
occupied_by: Some(WorkingDirectoryOccupancy {
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
display_name: "Coder".to_string(),
linked_at: "2026-08-12T00:00:00Z".to_string(),
}),
}],
diagnostics: vec![],
};
let value = serde_json::to_value(response).unwrap();
assert_eq!(
value["items"][0]["occupied_by"]["worker_id"],
"worker-opaque-64"
);
assert!(
value["items"][0]["occupied_by"]
.get("runtime_worker_id")
.is_none()
);
}
async fn get_json(app: Router, uri: &str) -> Value {
let response = app
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())