fix: keep execution workspace paths internal
This commit is contained in:
parent
8b7a5da031
commit
c90ebf2468
|
|
@ -388,6 +388,7 @@ fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState {
|
||||||
profile: Some(selector),
|
profile: Some(selector),
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -263,13 +263,26 @@ pub struct WorkerLookupResult {
|
||||||
/// Browser-safe worker spawn request shape.
|
/// Browser-safe worker spawn request shape.
|
||||||
///
|
///
|
||||||
/// The request carries Browser-facing launch semantics only: workspace intent,
|
/// The request carries Browser-facing launch semantics only: workspace intent,
|
||||||
/// optional display identity, acceptance policy, optional profile selector, and
|
/// optional display identity, acceptance policy, optional profile selector,
|
||||||
/// optional initial input. Runtime execution authority is resolved by the host
|
/// optional initial input, and optional configured Repository selector for
|
||||||
|
/// execution-workspace materialization. Runtime execution authority is resolved by the host
|
||||||
/// into a synced ConfigBundle before the canonical Runtime create request is
|
/// into a synced ConfigBundle before the canonical Runtime create request is
|
||||||
/// built. Raw workspace roots, child cwd, executable paths, tool scope,
|
/// built. Raw workspace roots, child cwd, executable paths, tool scope,
|
||||||
/// credentials, raw config stores, sockets, sessions, and storage paths are not
|
/// credentials, raw config stores, sockets, sessions, and storage paths are not
|
||||||
/// accepted from Workspace API callers.
|
/// accepted from Workspace API callers.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkerSpawnExecutionWorkspaceRequest {
|
||||||
|
/// Safe configured Repository id. The host resolves this id to repository
|
||||||
|
/// authority from server-side config; browser callers cannot provide raw
|
||||||
|
/// source paths or runtime-internal storage paths.
|
||||||
|
pub repository_id: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkerSpawnRequest {
|
pub struct WorkerSpawnRequest {
|
||||||
pub intent: WorkerSpawnIntent,
|
pub intent: WorkerSpawnIntent,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -279,8 +292,13 @@ 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 execution-workspace selector. The Workspace server resolves
|
||||||
|
/// this into a runtime-internal `ExecutionWorkspaceRequest` from configured
|
||||||
|
/// repositories before calling a host.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub execution_workspace: Option<ExecutionWorkspaceRequest>,
|
pub execution_workspace: Option<WorkerSpawnExecutionWorkspaceRequest>,
|
||||||
|
#[serde(skip, default)]
|
||||||
|
pub resolved_execution_workspace: Option<ExecutionWorkspaceRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
@ -1323,7 +1341,7 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
|
||||||
profile,
|
profile,
|
||||||
config_bundle,
|
config_bundle,
|
||||||
initial_input: request.initial_input.clone(),
|
initial_input: request.initial_input.clone(),
|
||||||
execution_workspace: request.execution_workspace.clone(),
|
execution_workspace: request.resolved_execution_workspace.clone(),
|
||||||
};
|
};
|
||||||
match self.runtime.create_worker(create_request) {
|
match self.runtime.create_worker(create_request) {
|
||||||
Ok(detail) => {
|
Ok(detail) => {
|
||||||
|
|
@ -1996,7 +2014,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
profile,
|
profile,
|
||||||
config_bundle,
|
config_bundle,
|
||||||
initial_input: request.initial_input.clone(),
|
initial_input: request.initial_input.clone(),
|
||||||
execution_workspace: request.execution_workspace.clone(),
|
execution_workspace: request.resolved_execution_workspace.clone(),
|
||||||
};
|
};
|
||||||
match self.post_json::<_, RuntimeHttpWorkerResponse>("/v1/workers", &create) {
|
match self.post_json::<_, RuntimeHttpWorkerResponse>("/v1/workers", &create) {
|
||||||
Ok(response) => WorkerSpawnResult {
|
Ok(response) => WorkerSpawnResult {
|
||||||
|
|
@ -3153,6 +3171,7 @@ mod tests {
|
||||||
profile: None,
|
profile: None,
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3283,6 +3302,7 @@ mod tests {
|
||||||
profile: None,
|
profile: None,
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -3383,6 +3403,7 @@ mod tests {
|
||||||
profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())),
|
profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())),
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -3412,6 +3433,7 @@ mod tests {
|
||||||
profile: None,
|
profile: None,
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ use crate::hosts::{
|
||||||
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
|
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
|
||||||
RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerInputRequest, WorkerInputResult,
|
RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerInputRequest, WorkerInputResult,
|
||||||
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState,
|
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState,
|
||||||
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult,
|
WorkerSpawnAcceptanceRequirement, WorkerSpawnExecutionWorkspaceRequest, WorkerSpawnIntent,
|
||||||
WorkerSummary, WorkerTranscriptProjection,
|
WorkerSpawnRequest, WorkerSpawnResult, WorkerSummary, WorkerTranscriptProjection,
|
||||||
};
|
};
|
||||||
use crate::identity::WorkspaceIdentity;
|
use crate::identity::WorkspaceIdentity;
|
||||||
use crate::observation::{
|
use crate::observation::{
|
||||||
|
|
@ -917,6 +917,51 @@ async fn get_worker_launch_options(
|
||||||
Ok(Json(worker_launch_options_response(&api)))
|
Ok(Json(worker_launch_options_response(&api)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execution_workspace_request_from_repository(
|
||||||
|
repository: &ConfiguredRepository,
|
||||||
|
selector: Option<&str>,
|
||||||
|
) -> ExecutionWorkspaceRequest {
|
||||||
|
ExecutionWorkspaceRequest {
|
||||||
|
repository: ExecutionWorkspaceRepository {
|
||||||
|
id: repository.id.clone(),
|
||||||
|
provider: repository.provider.clone(),
|
||||||
|
uri: repository.uri.clone(),
|
||||||
|
local_path: Some(repository.path.clone()),
|
||||||
|
selector: selector
|
||||||
|
.map(|selector| RuntimeRepositorySelector::from(selector.to_string()))
|
||||||
|
.or_else(|| {
|
||||||
|
repository
|
||||||
|
.default_selector
|
||||||
|
.clone()
|
||||||
|
.map(RuntimeRepositorySelector)
|
||||||
|
})
|
||||||
|
.or_else(|| Some(RuntimeRepositorySelector::from("HEAD"))),
|
||||||
|
},
|
||||||
|
materializer: MaterializerKind::LocalGitWorktree,
|
||||||
|
dirty_state_policy: DirtyStatePolicy::CleanPointOnly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_execution_workspace_request(
|
||||||
|
config: &ServerConfig,
|
||||||
|
request: &WorkerSpawnExecutionWorkspaceRequest,
|
||||||
|
) -> Result<ExecutionWorkspaceRequest> {
|
||||||
|
let repository = config
|
||||||
|
.repositories
|
||||||
|
.iter()
|
||||||
|
.find(|repository| repository.id == request.repository_id)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Config(format!(
|
||||||
|
"unknown repository id `{}` for Worker execution workspace",
|
||||||
|
request.repository_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(execution_workspace_request_from_repository(
|
||||||
|
repository,
|
||||||
|
request.selector.as_deref(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn default_execution_workspace_request(
|
fn default_execution_workspace_request(
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
repository_id: Option<&str>,
|
repository_id: Option<&str>,
|
||||||
|
|
@ -941,21 +986,9 @@ fn default_execution_workspace_request(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(ExecutionWorkspaceRequest {
|
Ok(Some(execution_workspace_request_from_repository(
|
||||||
repository: ExecutionWorkspaceRepository {
|
repository, None,
|
||||||
id: repository.id.clone(),
|
)))
|
||||||
provider: repository.provider.clone(),
|
|
||||||
uri: repository.uri.clone(),
|
|
||||||
local_path: Some(repository.path.clone()),
|
|
||||||
selector: repository
|
|
||||||
.default_selector
|
|
||||||
.clone()
|
|
||||||
.map(RuntimeRepositorySelector)
|
|
||||||
.or_else(|| Some(RuntimeRepositorySelector::from("HEAD"))),
|
|
||||||
},
|
|
||||||
materializer: MaterializerKind::LocalGitWorktree,
|
|
||||||
dirty_state_policy: DirtyStatePolicy::CleanPointOnly,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_workspace_worker(
|
async fn create_workspace_worker(
|
||||||
|
|
@ -997,7 +1030,8 @@ async fn create_workspace_worker(
|
||||||
},
|
},
|
||||||
profile: Some(profile_selector),
|
profile: Some(profile_selector),
|
||||||
initial_input,
|
initial_input,
|
||||||
execution_workspace,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: execution_workspace,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|err| err.into_error())?;
|
.map_err(|err| err.into_error())?;
|
||||||
|
|
@ -1082,8 +1116,15 @@ struct RuntimeConfigBundleAvailabilityQuery {
|
||||||
async fn create_runtime_worker(
|
async fn create_runtime_worker(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(runtime_id): AxumPath<String>,
|
AxumPath(runtime_id): AxumPath<String>,
|
||||||
Json(request): Json<WorkerSpawnRequest>,
|
Json(mut request): Json<WorkerSpawnRequest>,
|
||||||
) -> ApiResult<Json<WorkerSpawnResult>> {
|
) -> ApiResult<Json<WorkerSpawnResult>> {
|
||||||
|
request.resolved_execution_workspace = request
|
||||||
|
.execution_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map(|execution_workspace| {
|
||||||
|
configured_execution_workspace_request(&api.config, execution_workspace)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
let result = api
|
let result = api
|
||||||
.runtime
|
.runtime
|
||||||
.spawn_worker(&runtime_id, request)
|
.spawn_worker(&runtime_id, request)
|
||||||
|
|
@ -2895,6 +2936,71 @@ mod tests {
|
||||||
assert!(!projected.contains("http://"));
|
assert!(!projected.contains("http://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn runtime_worker_spawn_rejects_raw_execution_workspace_fields() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let app = test_app(dir.path()).await;
|
||||||
|
let response = request_json(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
"/api/runtimes/embedded-worker-runtime/workers",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"intent": {
|
||||||
|
"kind": "ticket_role",
|
||||||
|
"ticket_id": "00001KVZSGT0Q",
|
||||||
|
"role": "coder"
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"kind": "run_accepted",
|
||||||
|
"expected_segments": 0
|
||||||
|
},
|
||||||
|
"execution_workspace": {
|
||||||
|
"repository_id": TEST_REPOSITORY_ID,
|
||||||
|
"local_path": dir.path().display().to_string()
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("unknown field"),
|
||||||
|
"raw execution workspace field should be rejected: {response}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn runtime_worker_spawn_accepts_safe_repository_selector() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let app = test_app(dir.path()).await;
|
||||||
|
let response = request_json(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
"/api/runtimes/embedded-worker-runtime/workers",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"intent": {
|
||||||
|
"kind": "ticket_role",
|
||||||
|
"ticket_id": "00001KVZSGT0Q",
|
||||||
|
"role": "coder"
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"kind": "run_accepted",
|
||||||
|
"expected_segments": 0
|
||||||
|
},
|
||||||
|
"execution_workspace": {
|
||||||
|
"repository_id": TEST_REPOSITORY_ID,
|
||||||
|
"selector": "HEAD"
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let projected = serde_json::to_string(&response).unwrap();
|
||||||
|
assert!(!projected.contains(dir.path().to_string_lossy().as_ref()));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn browser_worker_create_rejects_extra_request_fields() {
|
async fn browser_worker_create_rejects_extra_request_fields() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -3308,6 +3414,7 @@ mod tests {
|
||||||
profile: None,
|
profile: None,
|
||||||
initial_input: None,
|
initial_input: None,
|
||||||
execution_workspace: None,
|
execution_workspace: None,
|
||||||
|
resolved_execution_workspace: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("spawn worker");
|
.expect("spawn worker");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user