merge: embedded no-workdir worker policy
This commit is contained in:
commit
2f7b809401
|
|
@ -753,6 +753,7 @@ pub struct WorkerLaunchRuntimeOption {
|
|||
pub display_name: String,
|
||||
pub built_in: bool,
|
||||
pub can_spawn_worker: bool,
|
||||
pub working_directory_required: bool,
|
||||
pub status: String,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
|
@ -2642,6 +2643,9 @@ async fn create_workspace_worker(
|
|||
relative_cwd: selection.relative_cwd,
|
||||
});
|
||||
validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
|
||||
if resolved_working_directory.is_none() {
|
||||
reject_no_workdir_for_non_embedded_runtime(&request.runtime_id)?;
|
||||
}
|
||||
let result = api
|
||||
.runtime
|
||||
.spawn_worker(
|
||||
|
|
@ -2811,11 +2815,36 @@ struct RuntimeConfigBundleAvailabilityQuery {
|
|||
digest: String,
|
||||
}
|
||||
|
||||
fn reject_no_workdir_for_non_embedded_runtime(
|
||||
runtime_id: &str,
|
||||
) -> std::result::Result<(), ApiError> {
|
||||
if runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
|
||||
return Ok(());
|
||||
}
|
||||
Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: runtime_id.to_string(),
|
||||
code: "workspace_worker_workdir_required".to_string(),
|
||||
message: "Only the embedded Runtime can launch a Worker without a working directory"
|
||||
.to_string(),
|
||||
},
|
||||
vec![RuntimeDiagnostic {
|
||||
code: "workspace_worker_workdir_required".to_string(),
|
||||
severity: DiagnosticSeverity::Error,
|
||||
message: "Select a working directory for this Runtime, or choose the embedded Runtime for a conversation-only Worker."
|
||||
.to_string(),
|
||||
}],
|
||||
))
|
||||
}
|
||||
|
||||
async fn create_runtime_worker(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(runtime_id): AxumPath<String>,
|
||||
Json(mut request): Json<WorkerSpawnRequest>,
|
||||
) -> ApiResult<Json<WorkerSpawnResult>> {
|
||||
if request.working_directory_request.is_none() && request.resolved_working_directory.is_none() {
|
||||
reject_no_workdir_for_non_embedded_runtime(&runtime_id)?;
|
||||
}
|
||||
request.resolved_working_directory_request = request
|
||||
.working_directory_request
|
||||
.as_ref()
|
||||
|
|
@ -3803,6 +3832,7 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> WorkerLaunchOptionsResp
|
|||
display_name: runtime.label,
|
||||
built_in,
|
||||
can_spawn_worker: runtime.capabilities.can_spawn_worker,
|
||||
working_directory_required: !built_in,
|
||||
status: runtime.status,
|
||||
diagnostics: runtime.diagnostics,
|
||||
}
|
||||
|
|
@ -4735,6 +4765,7 @@ impl IntoResponse for ApiError {
|
|||
|| code.starts_with("unsupported_worker_profile")
|
||||
|| code.starts_with("working_directory_")
|
||||
|| code.starts_with("workspace_cleanup_")
|
||||
|| code == "workspace_worker_workdir_required"
|
||||
|| code.ends_with("_already_exists")
|
||||
|| code.ends_with("_not_config_managed")
|
||||
|| code.ends_with("_unsupported") =>
|
||||
|
|
@ -6080,13 +6111,17 @@ mod tests {
|
|||
);
|
||||
|
||||
let launch_options = get_json(app.clone(), "/api/workers/launch-options").await;
|
||||
assert!(
|
||||
launch_options["runtimes"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
let runtimes = launch_options["runtimes"].as_array().unwrap();
|
||||
let embedded_runtime = runtimes
|
||||
.iter()
|
||||
.any(|runtime| runtime["runtime_id"] == "team-runtime")
|
||||
);
|
||||
.find(|runtime| runtime["runtime_id"] == EMBEDDED_WORKER_RUNTIME_ID)
|
||||
.expect("embedded runtime launch option");
|
||||
assert_eq!(embedded_runtime["working_directory_required"], false);
|
||||
let team_runtime = runtimes
|
||||
.iter()
|
||||
.find(|runtime| runtime["runtime_id"] == "team-runtime")
|
||||
.expect("team runtime launch option");
|
||||
assert_eq!(team_runtime["working_directory_required"], true);
|
||||
|
||||
let deleted = request_json(
|
||||
app.clone(),
|
||||
|
|
@ -6137,18 +6172,6 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
assert_eq!(added["restart_required"], false);
|
||||
let created = post_json(
|
||||
app.clone(),
|
||||
"/api/workers",
|
||||
serde_json::json!({
|
||||
"runtime_id": "busy-runtime",
|
||||
"display_name": "Remote Test Worker",
|
||||
"profile": "runtime_default",
|
||||
"initial_text": ""
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(created["runtime_id"], "busy-runtime");
|
||||
let workers = get_json(app.clone(), "/api/workers").await;
|
||||
assert!(
|
||||
workers["items"]
|
||||
|
|
@ -6352,6 +6375,38 @@ mod tests {
|
|||
assert!(!projected.contains("http://"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_worker_create_rejects_non_embedded_no_workdir() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let app = test_app(dir.path()).await;
|
||||
let response = request_json(
|
||||
app,
|
||||
"POST",
|
||||
"/api/workers",
|
||||
Some(serde_json::json!({
|
||||
"runtime_id": "remote-runtime",
|
||||
"display_name": "Remote Worker",
|
||||
"profile": "runtime_default",
|
||||
"initial_text": ""
|
||||
})),
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("workspace_worker_workdir_required")
|
||||
);
|
||||
assert!(
|
||||
response["diagnostics"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|diagnostic| { diagnostic["code"] == "workspace_worker_workdir_required" })
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_worker_spawn_rejects_raw_working_directory_fields() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export type WorkerLaunchRuntimeOption = {
|
|||
display_name: string;
|
||||
built_in: boolean;
|
||||
can_spawn_worker: boolean;
|
||||
working_directory_required: boolean;
|
||||
status: string;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const options: WorkerLaunchOptionsResponse = {
|
|||
status: "active",
|
||||
can_spawn_worker: true,
|
||||
built_in: false,
|
||||
working_directory_required: true,
|
||||
diagnostics: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -32,7 +33,8 @@ const options: WorkerLaunchOptionsResponse = {
|
|||
display_name: "Embedded",
|
||||
status: "active",
|
||||
can_spawn_worker: true,
|
||||
built_in: false,
|
||||
built_in: true,
|
||||
working_directory_required: false,
|
||||
diagnostics: [],
|
||||
},
|
||||
],
|
||||
|
|
@ -107,3 +109,23 @@ Deno.test("buildBrowserCreateWorkerRequest sends working_directory id and relati
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("buildBrowserCreateWorkerRequest omits working_directory for embedded no-workdir launches", () => {
|
||||
const request = buildBrowserCreateWorkerRequest({
|
||||
runtime_id: "embedded",
|
||||
display_name: "Worker",
|
||||
profile: "builtin:companion",
|
||||
initial_text: "chat",
|
||||
working_directory_id: "",
|
||||
working_directory_repository_id: "",
|
||||
working_directory_selector: "",
|
||||
relative_cwd: "",
|
||||
});
|
||||
|
||||
assertEquals(request, {
|
||||
runtime_id: "embedded",
|
||||
display_name: "Worker",
|
||||
profile: "builtin:companion",
|
||||
initial_text: "chat",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,14 @@
|
|||
let relativeCwd = $state('');
|
||||
let creatingWorkingDirectory = $state(false);
|
||||
let isNewWorkingDirectorySelected = $derived(workingDirectoryId === NEW_WORKING_DIRECTORY_VALUE);
|
||||
let canStartWorker = $derived(Boolean(runtimeId && profile && workingDirectoryId && !isNewWorkingDirectorySelected));
|
||||
let selectedRuntime = $derived(options?.runtimes.find((runtime) => runtime.runtime_id === runtimeId));
|
||||
let selectedRuntimeAllowsNoWorkdir = $derived(selectedRuntime?.working_directory_required === false);
|
||||
let hasSelectedExistingWorkdir = $derived(Boolean(workingDirectoryId && !isNewWorkingDirectorySelected));
|
||||
let canStartWorker = $derived(Boolean(
|
||||
runtimeId &&
|
||||
profile &&
|
||||
(hasSelectedExistingWorkdir || (selectedRuntimeAllowsNoWorkdir && !isNewWorkingDirectorySelected)),
|
||||
));
|
||||
|
||||
function workerApiPath(path: string): string {
|
||||
return workspaceApiPath(workspaceId, path);
|
||||
|
|
@ -81,7 +88,7 @@
|
|||
runtimeId = form.runtime_id;
|
||||
displayName = form.display_name;
|
||||
profile = form.profile;
|
||||
workingDirectoryId = form.working_directory_id || NEW_WORKING_DIRECTORY_VALUE;
|
||||
workingDirectoryId = form.working_directory_id;
|
||||
workingDirectoryRepositoryId = form.working_directory_repository_id;
|
||||
workingDirectorySelector = form.working_directory_selector;
|
||||
relativeCwd = form.relative_cwd;
|
||||
|
|
@ -149,8 +156,8 @@
|
|||
submitError = { message: 'workspace id is unavailable', diagnostics: [] };
|
||||
return;
|
||||
}
|
||||
if (isNewWorkingDirectorySelected || !workingDirectoryId) {
|
||||
submitError = { message: 'select or create a workdir before starting a Worker', diagnostics: [] };
|
||||
if (isNewWorkingDirectorySelected || (!workingDirectoryId && !selectedRuntimeAllowsNoWorkdir)) {
|
||||
submitError = { message: 'select or create a workdir before starting a Worker; only embedded Runtime can start without one', diagnostics: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +227,7 @@
|
|||
<header class="worker-new-page-header">
|
||||
<div>
|
||||
<h1 id="new-worker-heading">New Worker</h1>
|
||||
<p>Create a Worker on a selected Runtime and workdir.</p>
|
||||
<p>Create a Worker on a selected Runtime. Workdir-less conversation Workers are only available on embedded Runtime.</p>
|
||||
</div>
|
||||
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
|
||||
</header>
|
||||
|
|
@ -237,7 +244,11 @@
|
|||
<div class="worker-launch-sentence">
|
||||
<span>Run at</span>
|
||||
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Workdir">
|
||||
<option value="">Select workdir</option>
|
||||
{#if selectedRuntimeAllowsNoWorkdir}
|
||||
<option value="">No workdir · embedded conversation only</option>
|
||||
{:else}
|
||||
<option value="" disabled>Select workdir</option>
|
||||
{/if}
|
||||
{#each options?.working_directories ?? [] as directory}
|
||||
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
|
||||
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
||||
|
|
@ -259,6 +270,12 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
{#if !selectedRuntimeAllowsNoWorkdir && !workingDirectoryId && !isNewWorkingDirectorySelected}
|
||||
<p class="worker-workdir-note">This Runtime requires a selected workdir before starting a Worker.</p>
|
||||
{:else if selectedRuntimeAllowsNoWorkdir && !workingDirectoryId}
|
||||
<p class="worker-workdir-note">No filesystem tools or Bash will be available without a workdir.</p>
|
||||
{/if}
|
||||
|
||||
{#if isNewWorkingDirectorySelected}
|
||||
<div class="new-working-directory-panel">
|
||||
<h3>New workdir</h3>
|
||||
|
|
@ -286,10 +303,12 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasSelectedExistingWorkdir || isNewWorkingDirectorySelected}
|
||||
<label class="relative-cwd-field">
|
||||
<span>Relative cwd inside workdir</span>
|
||||
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside workdir" />
|
||||
</label>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="worker-form-section" aria-labelledby="worker-details-heading">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user