fix(code-queue): add workdirs contract checks

This commit is contained in:
Codex
2026-05-19 02:07:41 +00:00
parent 49cf1e57bb
commit 6006017393
9 changed files with 611 additions and 9 deletions
@@ -395,6 +395,7 @@ function codeQueueK3sServiceIdForRequest(method: string, targetPath: string): st
function codeQueueMasterControlPath(method: string, targetPath: string): boolean {
const normalizedMethod = method.toUpperCase();
if (targetPath === "/" || targetPath === "/health" || targetPath === "/live" || targetPath === "/logs") return true;
if (targetPath === "/api/workdirs" || /^\/api\/workdirs\/[^/]+\/[^/]+\/.+$/u.test(targetPath)) return true;
if (targetPath === "/api/queue-claim-move/self-test") return true;
if (targetPath === "/api/queues" || targetPath === "/api/queues/merge") return true;
if (/^\/api\/queues\/[^/]+(?:\/merge)?$/u.test(targetPath)) return true;
@@ -404,6 +405,38 @@ function codeQueueMasterControlPath(method: string, targetPath: string): boolean
return false;
}
function codeQueueCompatWorkdirsResponse(service: MicroserviceConfig, status: number, contentType: string, bodyText: string): Response | null {
if (status !== 404) return null;
let body: any = null;
try {
body = JSON.parse(bodyText);
} catch {
return null;
}
if (body?.path !== "/api/workdirs") return null;
const providerId = "D601";
return jsonResponse({
ok: true,
workdirs: [{
providerId,
executionMode: "default",
path: "/workspace",
createdAt: null,
updatedAt: null,
source: "backend-core-compat",
}],
defaultProviderId: providerId,
defaultWorkdir: "/workspace",
remoteDefaultWorkdir: "/home/ubuntu",
source: "backend-core-compat-code-queue-workdirs",
compat: {
reason: "code-queue-mgr version does not yet expose /api/workdirs",
upstreamStatus: status,
upstreamContentType: contentType,
},
}, 200);
}
// ---------------------------------------------------------------------------
// Cache helpers
// ---------------------------------------------------------------------------
@@ -1026,7 +1059,24 @@ async function fetchMicroserviceUpstreamResponse(
): Promise<Response> {
if (service.id === "code-queue" && codeQueueMasterControlPath(method, targetPath)) {
const mgr = microserviceById("code-queue-mgr");
if (mgr !== null) return directMicroserviceResponse(mgr, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
if (mgr !== null) {
const response = await directMicroserviceResponse(mgr, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
if (method.toUpperCase() === "GET" && targetPath === "/api/workdirs") {
const contentType = response.headers.get("content-type") ?? "text/plain; charset=utf-8";
const responseBody = await response.text();
const compat = codeQueueCompatWorkdirsResponse(service, response.status, contentType, responseBody);
if (compat !== null) return compat;
return new Response(responseBody, {
status: response.status,
headers: {
"content-type": contentType,
"x-unidesk-proxy-mode": response.headers.get("x-unidesk-proxy-mode") ?? "direct",
"x-unidesk-response-truncated": response.headers.get("x-unidesk-response-truncated") ?? "false",
},
});
}
return response;
}
logger("warn", "code_queue_mgr_missing_fallback_to_d601", { method, targetPath });
}
if (isK3sctlManagedMicroservice(service)) {
@@ -154,6 +154,47 @@ fn code_queue_scheduler_only_path(method: &Method, target_path: &str) -> bool {
&& !(target_path == "/" || target_path == "/health")
}
fn code_queue_fallback_workdirs_response(
service: &MicroserviceConfig,
status: u16,
content_type: &str,
body_text: &str,
) -> Option<Response> {
if status != 404 {
return None;
}
let body = serde_json::from_str::<Value>(body_text).ok()?;
if body.get("path").and_then(Value::as_str) != Some("/api/workdirs") {
return None;
}
let provider_id = service.provider_id.clone();
let fallback = json!({
"ok": true,
"workdirs": [{
"providerId": provider_id,
"executionMode": "default",
"path": "/workspace",
"createdAt": null,
"updatedAt": null,
"source": "backend-core-compat"
}],
"defaultProviderId": provider_id,
"defaultWorkdir": "/workspace",
"remoteDefaultWorkdir": "/home/ubuntu",
"source": "backend-core-compat-code-queue-workdirs",
"compat": {
"reason": "code-queue-mgr version does not yet expose /api/workdirs",
"upstreamStatus": status,
"upstreamContentType": content_type
}
});
Some(response_with_body(
200,
"application/json; charset=utf-8",
Body::from(serde_json::to_string(&fallback).unwrap_or_else(|_| "{\"ok\":true,\"workdirs\":[]}".to_string())),
))
}
fn path_matches_task_detail(path: &str) -> bool {
path.strip_prefix("/api/tasks/")
.is_some_and(|rest| !rest.is_empty() && !rest.contains('/'))
@@ -926,7 +967,7 @@ async fn fetch_microservice_upstream_response(
)
.await;
}
return direct_microservice_response(
let response = direct_microservice_response(
&direct_code_queue_mgr_service(service, None),
method,
target_path,
@@ -935,6 +976,18 @@ async fn fetch_microservice_upstream_response(
body_text,
)
.await;
if method == Method::GET && target_path == "/api/workdirs" {
let (status, content_type, response_body) = response_to_text(response).await.unwrap_or((
502,
"text/plain".to_string(),
"failed to read code-queue workdirs response".to_string(),
));
if let Some(fallback) = code_queue_fallback_workdirs_response(service, status, &content_type, &response_body) {
return fallback;
}
return response_with_body(status, &content_type, Body::from(response_body));
}
return response;
}
if is_k3sctl_managed_microservice(service) {
return k3sctl_adapter_microservice_response(
@@ -18,6 +18,7 @@ const MAX_BODY_BYTES: usize = 512 * 1024;
const MAX_LIST_LIMIT: i64 = 200;
const MAX_OUTPUT_PAGE_LIMIT: usize = 500;
const MAX_PREVIEW_CHARS: usize = 6000;
const WORKDIR_MAX_CHARS: usize = 512;
#[derive(Clone)]
struct Config {
@@ -347,6 +348,14 @@ fn ensure_schema(state: &AppState) -> Result<(), String> {
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS unidesk_code_queue_workdirs (
provider_id TEXT NOT NULL,
execution_mode TEXT NOT NULL DEFAULT 'default',
path TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (provider_id, execution_mode, path)
);
ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS queue_id TEXT NOT NULL DEFAULT 'default';
ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS provider_id TEXT NOT NULL DEFAULT 'main-server';
ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS execution_mode TEXT NOT NULL DEFAULT 'default';
@@ -355,6 +364,7 @@ fn ensure_schema(state: &AppState) -> Result<(), String> {
ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS reference_injection JSONB;
ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS read_at TIMESTAMPTZ;
ALTER TABLE unidesk_code_queue_queues ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';
ALTER TABLE unidesk_code_queue_workdirs ADD COLUMN IF NOT EXISTS execution_mode TEXT NOT NULL DEFAULT 'default';
CREATE INDEX IF NOT EXISTS idx_code_queue_tasks_list ON unidesk_code_queue_tasks (status, updated_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_code_queue_tasks_queue_updated ON unidesk_code_queue_tasks (queue_id, updated_at DESC, id DESC);
",
@@ -370,6 +380,7 @@ fn ensure_schema(state: &AppState) -> Result<(), String> {
&[&DEFAULT_QUEUE_ID.to_string(), &DEFAULT_QUEUE_ID.to_string(), &now],
)
.map_err(|error| error_detail(&error))?;
upsert_default_workdir(&mut client, state, &now)?;
Ok(())
}
@@ -1093,6 +1104,185 @@ fn update_queue(state: &AppState, queue_id_value: &str, request: &mut Request) -
))
}
fn default_workdir_record(state: &AppState, timestamp: &str) -> Result<Value, String> {
let provider_id = normalize_workdir_provider_id(state, &state.config.default_provider_id);
let path = normalize_workdir_path(state, &provider_id, &state.config.default_workdir)?;
Ok(json!({
"providerId": provider_id,
"executionMode": "default",
"path": path,
"createdAt": timestamp,
"updatedAt": timestamp
}))
}
fn workdir_key(record: &Value) -> String {
format!(
"{}\u{0}{}\u{0}{}",
record.get("providerId").and_then(Value::as_str).unwrap_or(""),
record.get("executionMode").and_then(Value::as_str).unwrap_or("default"),
record.get("path").and_then(Value::as_str).unwrap_or("")
)
}
fn sorted_workdir_records(records: Vec<Value>) -> Vec<Value> {
let mut keyed = std::collections::BTreeMap::<String, Value>::new();
for record in records {
keyed.insert(workdir_key(&record), record);
}
keyed.into_values().collect()
}
fn upsert_default_workdir(client: &mut Client, state: &AppState, timestamp: &str) -> Result<(), String> {
let record = default_workdir_record(state, timestamp)?;
let provider_id = record.get("providerId").and_then(Value::as_str).unwrap_or("").to_string();
let execution_mode = record.get("executionMode").and_then(Value::as_str).unwrap_or("default").to_string();
let path = record.get("path").and_then(Value::as_str).unwrap_or("").to_string();
client
.execute(
"
INSERT INTO unidesk_code_queue_workdirs (provider_id, execution_mode, path, created_at, updated_at)
VALUES ($1, $2, $3, $4::text::timestamptz, $4::text::timestamptz)
ON CONFLICT (provider_id, execution_mode, path) DO UPDATE SET updated_at = EXCLUDED.updated_at
",
&[&provider_id, &execution_mode, &path, &timestamp],
)
.map_err(|error| error_detail(&error))?;
Ok(())
}
fn load_workdirs(state: &AppState) -> Result<Vec<Value>, String> {
let mut client = db_client(state, "unidesk-code-queue-mgr-rust-workdirs")?;
let rows = client
.query(
"
SELECT provider_id, execution_mode, path, created_at::text, updated_at::text
FROM unidesk_code_queue_workdirs
ORDER BY provider_id ASC, execution_mode ASC, path ASC
",
&[],
)
.map_err(|error| error_detail(&error))?;
let mut records = Vec::new();
if let Ok(record) = default_workdir_record(state, &now_iso()) {
records.push(record);
}
for row in rows {
let provider_id = normalize_workdir_provider_id(state, row.get::<_, &str>(0));
let execution_mode_raw: &str = row.get(1);
let execution_mode = match execution_mode_raw {
"windows-native" => "windows-native",
_ => "default",
};
let path_raw: &str = row.get(2);
let path = normalize_workdir_path(state, &provider_id, path_raw)?;
records.push(json!({
"providerId": provider_id,
"executionMode": execution_mode,
"path": path,
"createdAt": row.get::<_, String>(3),
"updatedAt": row.get::<_, String>(4)
}));
}
Ok(sorted_workdir_records(records))
}
fn list_workdirs(state: &AppState, url: &RequestUrl) -> Result<(Value, u16), (Value, u16)> {
let provider_filter = query_param(url, "providerId")
.filter(|value| !value.trim().is_empty())
.map(|value| normalize_workdir_provider_id(state, &value));
let mode_filter = query_param(url, "executionMode")
.filter(|value| !value.trim().is_empty())
.map(|value| normalize_execution_mode(Some(&json!(value))));
let records = load_workdirs(state).map_err(|error| (json!({ "ok": false, "error": error }), 500))?;
let workdirs = records
.into_iter()
.filter(|record| {
if let Some(provider_id) = &provider_filter {
if record.get("providerId").and_then(Value::as_str) != Some(provider_id.as_str()) {
return false;
}
}
if let Some(mode) = &mode_filter {
if record.get("executionMode").and_then(Value::as_str) != Some(mode.as_str()) {
return false;
}
}
true
})
.collect::<Vec<_>>();
Ok((json!({
"ok": true,
"workdirs": workdirs,
"defaultProviderId": state.config.default_provider_id,
"defaultWorkdir": state.config.default_workdir,
"remoteDefaultWorkdir": state.config.remote_default_workdir,
"source": "code-queue-mgr-rust-postgres"
}), 200))
}
fn create_workdir(state: &AppState, request: &mut Request) -> Result<(Value, u16), (Value, u16)> {
let body = read_json_body(request)?;
let provider_id = normalize_workdir_provider_id(state, body.get("providerId").and_then(Value::as_str).unwrap_or(""));
let execution_mode = normalize_execution_mode(body.get("executionMode"));
let raw_path = body
.get("path")
.or_else(|| body.get("cwd"))
.or_else(|| body.get("workdir"))
.and_then(Value::as_str)
.unwrap_or("");
let path = normalize_workdir_path(state, &provider_id, raw_path).map_err(|error| (json!({ "ok": false, "error": error }), 400))?;
let now = now_iso();
let mut client = db_client(state, "unidesk-code-queue-mgr-rust-workdir-create").map_err(|error| (json!({ "ok": false, "error": error }), 500))?;
client
.execute(
"
INSERT INTO unidesk_code_queue_workdirs (provider_id, execution_mode, path, created_at, updated_at)
VALUES ($1, $2, $3, $4::text::timestamptz, $4::text::timestamptz)
ON CONFLICT (provider_id, execution_mode, path) DO UPDATE SET updated_at = EXCLUDED.updated_at
",
&[&provider_id, &execution_mode, &path, &now],
)
.map_err(|error| (json!({ "ok": false, "error": error_detail(&error) }), 500))?;
let workdir = json!({ "providerId": provider_id, "executionMode": execution_mode, "path": path, "createdAt": now, "updatedAt": now });
let workdirs = load_workdirs(state).map_err(|error| (json!({ "ok": false, "error": error }), 500))?;
Ok((json!({
"ok": true,
"workdir": workdir,
"workdirs": workdirs,
"ensure": { "ok": true, "skipped": true, "reason": "code-queue-mgr records workdir menu options only; provider directories are created by the execution plane" }
}), 201))
}
fn delete_workdir(state: &AppState, provider_id_value: &str, execution_mode_value: &str, path_value: &str) -> Result<(Value, u16), (Value, u16)> {
let provider_id = normalize_workdir_provider_id(state, provider_id_value);
let execution_mode = normalize_execution_mode(Some(&json!(execution_mode_value)));
let path = normalize_workdir_path(state, &provider_id, path_value).map_err(|error| (json!({ "ok": false, "error": error }), 400))?;
let mut client = db_client(state, "unidesk-code-queue-mgr-rust-workdir-delete").map_err(|error| (json!({ "ok": false, "error": error }), 500))?;
let rows = client
.query(
"
DELETE FROM unidesk_code_queue_workdirs
WHERE provider_id = $1 AND execution_mode = $2 AND path = $3
RETURNING provider_id, execution_mode, path, created_at::text, updated_at::text
",
&[&provider_id, &execution_mode, &path],
)
.map_err(|error| (json!({ "ok": false, "error": error_detail(&error) }), 500))?;
let Some(row) = rows.first() else {
return Ok((json!({ "ok": false, "error": "workdir not found" }), 404));
};
let deleted = json!({
"providerId": row.get::<_, String>(0),
"executionMode": row.get::<_, String>(1),
"path": row.get::<_, String>(2),
"createdAt": row.get::<_, String>(3),
"updatedAt": row.get::<_, String>(4)
});
let workdirs = load_workdirs(state).map_err(|error| (json!({ "ok": false, "error": error }), 500))?;
Ok((json!({ "ok": true, "deleted": deleted, "workdirs": workdirs }), 200))
}
fn normalize_provider_id(state: &AppState, value: Option<&Value>) -> String {
value
.and_then(Value::as_str)
@@ -1102,6 +1292,20 @@ fn normalize_provider_id(state: &AppState, value: Option<&Value>) -> String {
.to_string()
}
fn normalize_workdir_provider_id(state: &AppState, value: &str) -> String {
let text = value.trim();
if !text.is_empty()
&& text.len() <= 64
&& text
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '-'))
{
text.to_string()
} else {
state.config.default_provider_id.clone()
}
}
fn normalize_cwd(state: &AppState, provider_id: &str, value: Option<&Value>) -> String {
value
.and_then(Value::as_str)
@@ -1111,6 +1315,34 @@ fn normalize_cwd(state: &AppState, provider_id: &str, value: Option<&Value>) ->
.unwrap_or_else(|| if provider_id == "main-server" { state.config.default_workdir.clone() } else { state.config.remote_default_workdir.clone() })
}
fn default_workdir_for_provider(state: &AppState, provider_id: &str) -> String {
if provider_id == state.config.default_provider_id {
state.config.default_workdir.clone()
} else {
state.config.remote_default_workdir.clone()
}
}
fn normalize_workdir_path(state: &AppState, provider_id: &str, value: &str) -> Result<String, String> {
let raw = value.trim();
if raw.is_empty() {
return Err("workdir path is required".to_string());
}
if raw.chars().count() > WORKDIR_MAX_CHARS {
return Err(format!("workdir path must be {WORKDIR_MAX_CHARS} characters or fewer"));
}
if raw.contains('\0') {
return Err("workdir path contains an invalid character".to_string());
}
let path = if raw.starts_with('/') {
raw.to_string()
} else {
format!("{}/{}", default_workdir_for_provider(state, provider_id).trim_end_matches('/'), raw)
};
let trimmed = path.trim_end_matches('/').to_string();
Ok(if trimmed.is_empty() { "/".to_string() } else { trimmed })
}
fn normalize_execution_mode(value: Option<&Value>) -> String {
match value.and_then(Value::as_str).unwrap_or("").trim().to_ascii_lowercase().as_str() {
"windows-native" | "windows" | "win32" | "native-windows" => "windows-native".to_string(),
@@ -1924,7 +2156,7 @@ fn health(state: &AppState) -> (Value, u16) {
"readPath": "column-first"
},
"endpoints": {
"control": ["/api/queues", "/api/tasks", "/api/tasks/:id/(move|retry|read|edit)", "/api/queue-claim-move/self-test"],
"control": ["/api/queues", "/api/tasks", "/api/tasks/:id/(move|retry|read|edit)", "/api/workdirs", "/api/queue-claim-move/self-test"],
"traceRead": ["/api/tasks/overview", "/api/tasks/:id/summary", "/api/tasks/:id/trace-summary", "/api/tasks/:id/trace-steps", "/api/tasks/:id/output"]
}
});
@@ -2015,6 +2247,29 @@ fn handle_request(state: &AppState, request: &mut Request) -> (Value, u16) {
if path == "/api/queue-claim-move/self-test" && (method == Method::Get || method == Method::Post) {
return (json!({ "ok": true, "runtime": "rust", "check": "route-present", "claimMoveSafety": "conditional update requires unclaimed queued/retry_wait task" }), 200);
}
if path == "/api/workdirs" && method == Method::Get {
return list_workdirs(state, &url).unwrap_or_else(|response| response);
}
if path == "/api/workdirs" && method == Method::Post {
return create_workdir(state, request).unwrap_or_else(|response| response);
}
if method == Method::Delete {
if let Some(rest) = path.strip_prefix("/api/workdirs/") {
let mut parts = rest.splitn(3, '/');
let provider_id = parts.next().unwrap_or_default();
let execution_mode = parts.next().unwrap_or_default();
let path_value = parts.next().unwrap_or_default();
if !provider_id.is_empty() && !execution_mode.is_empty() && !path_value.is_empty() {
return delete_workdir(
state,
&percent_decode(provider_id),
&percent_decode(execution_mode),
&percent_decode(path_value),
)
.unwrap_or_else(|response| response);
}
}
}
if let Some(response) = route_active_control(path, &method) {
return response;
}
@@ -2344,4 +2599,13 @@ mod tests {
assert!(preview.get("text").and_then(Value::as_str).unwrap_or("").contains("...<truncated>"));
assert!(!preview.get("text").and_then(Value::as_str).unwrap_or("").contains(marker));
}
#[test]
fn default_workdir_contract_includes_workspace() {
let state = test_state();
let record = default_workdir_record(&state, "2026-01-01T00:00:00.000Z").expect("default workdir");
assert_eq!(record.get("providerId").and_then(Value::as_str), Some("D601"));
assert_eq!(record.get("executionMode").and_then(Value::as_str), Some("default"));
assert_eq!(record.get("path").and_then(Value::as_str), Some("/workspace"));
}
}
@@ -40,6 +40,14 @@ interface QueueRecord {
updatedAt: string;
}
interface WorkdirRecord {
providerId: string;
executionMode: CodeExecutionMode;
path: string;
createdAt: string;
updatedAt: string;
}
interface DailyTaskStatsBucket {
date: string;
executedTasks: number;
@@ -275,6 +283,7 @@ const codeQueueEnvironmentHint = [
"如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。",
].join("\n");
const maxTaskAttempts = 99;
const workdirMaxLength = 512;
const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", "minimax-m2.7"];
const codeExecutionModes: CodeExecutionMode[] = ["default", "windows-native"];
const codexStatsTimeZone = "Asia/Shanghai";
@@ -523,10 +532,140 @@ function normalizeProviderId(value: unknown): string {
return text.length > 0 ? text : config.defaultProviderId;
}
function normalizeWorkdirProviderId(value: unknown): string {
const text = typeof value === "string" ? value.trim() : "";
return /^[A-Za-z0-9_.-]{1,64}$/u.test(text) ? text : config.defaultProviderId;
}
function normalizeCwd(providerId: string, value: unknown): string {
const text = typeof value === "string" && value.trim().length > 0 ? value.trim() : "";
if (text.length > 0) return text;
return providerId === "main-server" ? config.defaultWorkdir : config.remoteDefaultWorkdir;
return providerId === config.defaultProviderId ? config.defaultWorkdir : config.remoteDefaultWorkdir;
}
function defaultWorkdirForProvider(providerId: string): string {
return providerId === config.defaultProviderId ? config.defaultWorkdir : config.remoteDefaultWorkdir;
}
function normalizeWorkdirPath(value: unknown, providerId: string): string {
const raw = typeof value === "string" ? value.trim() : "";
if (raw.length === 0) throw new Error("workdir path is required");
if (raw.length > workdirMaxLength) throw new Error(`workdir path must be ${workdirMaxLength} characters or fewer`);
if (raw.includes("\u0000")) throw new Error("workdir path contains an invalid character");
if (raw.startsWith("/")) return raw.replace(/\/+$/u, "") || "/";
return `${defaultWorkdirForProvider(providerId).replace(/\/+$/u, "")}/${raw}`.replace(/\/+$/u, "") || "/";
}
function defaultWorkdirRecords(timestamp = nowIso()): WorkdirRecord[] {
const providerId = normalizeWorkdirProviderId(config.defaultProviderId);
return [{
providerId,
executionMode: "default",
path: normalizeWorkdirPath(config.defaultWorkdir, providerId),
createdAt: timestamp,
updatedAt: timestamp,
}];
}
function workdirRecordKey(record: Pick<WorkdirRecord, "providerId" | "executionMode" | "path">): string {
return `${record.providerId}\u0000${record.executionMode}\u0000${record.path}`;
}
function mergeWorkdirRecords(records: WorkdirRecord[]): WorkdirRecord[] {
const byKey = new Map<string, WorkdirRecord>();
for (const record of records) {
byKey.set(workdirRecordKey(record), record);
}
return Array.from(byKey.values()).sort((left, right) => {
const providerDelta = left.providerId.localeCompare(right.providerId);
if (providerDelta !== 0) return providerDelta;
const modeDelta = left.executionMode.localeCompare(right.executionMode);
if (modeDelta !== 0) return modeDelta;
return left.path.localeCompare(right.path);
});
}
async function loadWorkdirRecords(): Promise<WorkdirRecord[]> {
const rows = await mgrSql<Array<{ provider_id: string; execution_mode: string; path: string; created_at: Date | string; updated_at: Date | string }>>`
SELECT provider_id, execution_mode, path, created_at, updated_at
FROM unidesk_code_queue_workdirs
ORDER BY provider_id ASC, execution_mode ASC, path ASC
`;
const records = rows.map((row) => {
const providerId = normalizeWorkdirProviderId(row.provider_id);
return {
providerId,
executionMode: normalizeExecutionMode(row.execution_mode),
path: normalizeWorkdirPath(row.path, providerId),
createdAt: timestampToIso(row.created_at) ?? nowIso(),
updatedAt: timestampToIso(row.updated_at) ?? nowIso(),
};
});
return mergeWorkdirRecords([...defaultWorkdirRecords(), ...records]);
}
async function upsertWorkdir(record: WorkdirRecord): Promise<void> {
await mgrSql`
INSERT INTO unidesk_code_queue_workdirs (provider_id, execution_mode, path, created_at, updated_at)
VALUES (${record.providerId}, ${record.executionMode}, ${record.path}, ${record.createdAt}, ${record.updatedAt})
ON CONFLICT (provider_id, execution_mode, path) DO UPDATE SET
updated_at = EXCLUDED.updated_at
`;
}
function filteredWorkdirRecords(records: WorkdirRecord[], url: URL): WorkdirRecord[] {
const providerIdParam = url.searchParams.get("providerId");
const executionModeParam = url.searchParams.get("executionMode");
const providerId = providerIdParam === null || providerIdParam.trim().length === 0 ? null : normalizeWorkdirProviderId(providerIdParam);
const executionMode = executionModeParam === null || executionModeParam.trim().length === 0 ? null : normalizeExecutionMode(executionModeParam);
return records.filter((record) => {
if (providerId !== null && record.providerId !== providerId) return false;
if (executionMode !== null && record.executionMode !== executionMode) return false;
return true;
});
}
async function listWorkdirs(url: URL): Promise<Response> {
const records = await loadWorkdirRecords();
return jsonResponse({
ok: true,
workdirs: filteredWorkdirRecords(records, url),
defaultProviderId: config.defaultProviderId,
defaultWorkdir: config.defaultWorkdir,
remoteDefaultWorkdir: config.remoteDefaultWorkdir,
source: "code-queue-mgr-postgres",
});
}
async function createWorkdir(req: Request): Promise<Response> {
const body = asRecord(await readJson(req)) ?? {};
const providerId = normalizeWorkdirProviderId(body.providerId);
const executionMode = normalizeExecutionMode(body.executionMode);
const path = normalizeWorkdirPath(body.path ?? body.cwd ?? body.workdir, providerId);
const now = nowIso();
const workdir: WorkdirRecord = { providerId, executionMode, path, createdAt: now, updatedAt: now };
await upsertWorkdir(workdir);
return jsonResponse({
ok: true,
workdir,
workdirs: await loadWorkdirRecords(),
ensure: { ok: true, skipped: true, reason: "code-queue-mgr records workdir menu options only; provider directories are created by the execution plane" },
}, 201);
}
async function deleteWorkdir(providerIdValue: string, executionModeValue: string, pathValue: string): Promise<Response> {
const providerId = normalizeWorkdirProviderId(providerIdValue);
const executionMode = normalizeExecutionMode(executionModeValue);
const path = normalizeWorkdirPath(pathValue, providerId);
const rows = await mgrSql<WorkdirRecord[]>`
DELETE FROM unidesk_code_queue_workdirs
WHERE provider_id = ${providerId}
AND execution_mode = ${executionMode}
AND path = ${path}
RETURNING provider_id AS "providerId", execution_mode AS "executionMode", path, created_at AS "createdAt", updated_at AS "updatedAt"
`;
if (rows.length === 0) return jsonResponse({ ok: false, error: "workdir not found" }, 404);
return jsonResponse({ ok: true, deleted: rows[0], workdirs: await loadWorkdirRecords() });
}
function normalizeStringArray(value: unknown): string[] {
@@ -1145,6 +1284,16 @@ async function ensureSchema(): Promise<void> {
updated_at TIMESTAMPTZ NOT NULL
)
`;
await mgrSql`
CREATE TABLE IF NOT EXISTS unidesk_code_queue_workdirs (
provider_id TEXT NOT NULL,
execution_mode TEXT NOT NULL DEFAULT 'default',
path TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (provider_id, execution_mode, path)
)
`;
await mgrSql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS queue_id TEXT NOT NULL DEFAULT 'default'`;
await mgrSql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS provider_id TEXT NOT NULL DEFAULT 'main-server'`;
await mgrSql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS execution_mode TEXT NOT NULL DEFAULT 'default'`;
@@ -1153,8 +1302,10 @@ async function ensureSchema(): Promise<void> {
await mgrSql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS reference_injection JSONB`;
await mgrSql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS read_at TIMESTAMPTZ`;
await mgrSql`ALTER TABLE unidesk_code_queue_queues ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT ''`;
await mgrSql`ALTER TABLE unidesk_code_queue_workdirs ADD COLUMN IF NOT EXISTS execution_mode TEXT NOT NULL DEFAULT 'default'`;
const now = nowIso();
await upsertQueue({ id: config.defaultQueueId, name: config.defaultQueueId, createdAt: now, updatedAt: now });
await Promise.all(defaultWorkdirRecords(now).map((record) => upsertWorkdir(record)));
schemaReady = true;
schemaLastError = null;
}
@@ -2639,7 +2790,7 @@ async function route(req: Request): Promise<Response> {
noDockerSocket: true,
},
endpoints: {
control: ["/api/queues", "/api/tasks", "/api/tasks/:id/(move|retry|read|edit)", "/api/queue-claim-move/self-test"],
control: ["/api/queues", "/api/tasks", "/api/tasks/:id/(move|retry|read|edit)", "/api/workdirs", "/api/queue-claim-move/self-test"],
traceRead: ["/api/tasks/overview", "/api/tasks/:id/summary", "/api/tasks/:id/trace-summary", "/api/tasks/:id/trace-steps", "/api/tasks/:id/output"],
},
}, schemaReady ? 200 : 503);
@@ -2649,6 +2800,10 @@ async function route(req: Request): Promise<Response> {
if (!schemaReady) return jsonResponse({ ok: false, error: "code-queue-mgr database schema is not ready", schemaLastError }, 503);
return jsonResponse(await runQueueClaimMoveSelfTest(), 200);
}
if (url.pathname === "/api/workdirs" && req.method === "GET") return await listWorkdirs(url);
if (url.pathname === "/api/workdirs" && req.method === "POST") return await createWorkdir(req);
const workdirMatch = url.pathname.match(/^\/api\/workdirs\/([^/]+)\/([^/]+)\/(.+)$/u);
if (workdirMatch !== null && req.method === "DELETE") return await deleteWorkdir(decodeURIComponent(workdirMatch[1] ?? ""), decodeURIComponent(workdirMatch[2] ?? ""), decodeURIComponent(workdirMatch[3] ?? ""));
const activeControl = routeActiveControl(url.pathname, req.method);
if (activeControl !== null) return jsonResponse(activeControl.body, activeControl.status);
if (url.pathname === "/api/queues" && req.method === "GET") {
@@ -255,6 +255,56 @@ spec:
bun install --frozen-lockfile
(cd src && bun install --frozen-lockfile)
UNIDESK_D601_RUST_CHECK=1 bun scripts/cli.ts check --full --rust
cargo build --manifest-path src/components/microservices/code-queue-mgr/Cargo.toml --release
CQ_MGR_ID="$(printf '%s' "$(params.revision)" | tr -cd 'A-Za-z0-9_.-' | cut -c1-48)"
CQ_MGR_DB="$(workspaces.source.path)/code-queue-mgr-workdirs-postgres"
rm -rf "$CQ_MGR_DB"
mkdir -p "$CQ_MGR_DB"
docker rm -f "code-queue-mgr-workdirs-$CQ_MGR_ID" >/dev/null 2>&1 || true
docker run -d --name "code-queue-mgr-workdirs-$CQ_MGR_ID" \
-p 127.0.0.1::5432 \
-e POSTGRES_USER=unidesk_ci \
-e POSTGRES_PASSWORD=unidesk_ci_password \
-e POSTGRES_DB=unidesk_ci \
-v "$CQ_MGR_DB:/var/lib/postgresql/data" \
postgres:16-alpine >/tmp/code-queue-mgr-workdirs-postgres.cid
CQ_MGR_DB_PORT="$(docker inspect "code-queue-mgr-workdirs-$CQ_MGR_ID" --format '{{(index (index .NetworkSettings.Ports "5432/tcp") 0).HostPort}}')"
cleanup() {
docker rm -f "code-queue-mgr-workdirs-$CQ_MGR_ID" >/dev/null 2>&1 || true
}
trap cleanup EXIT
for _ in $(seq 1 60); do
if docker exec "code-queue-mgr-workdirs-$CQ_MGR_ID" pg_isready -U unidesk_ci -d unidesk_ci >/dev/null 2>&1; then
break
fi
sleep 1
done
CQ_MGR_PORT="$(python3 - <<'PY'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PY
)"
DATABASE_URL="postgres://unidesk_ci:unidesk_ci_password@127.0.0.1:$CQ_MGR_DB_PORT/unidesk_ci" \
HOST=127.0.0.1 \
PORT="$CQ_MGR_PORT" \
CODE_QUEUE_MAIN_PROVIDER_ID=D601 \
CODE_QUEUE_WORKDIR=/workspace \
CODE_QUEUE_REMOTE_WORKDIR=/home/ubuntu \
src/components/microservices/code-queue-mgr/target/release/code-queue-mgr >"$(workspaces.source.path)/code-queue-mgr-workdirs.log" 2>&1 &
cq_mgr_pid="$!"
trap 'kill "$cq_mgr_pid" >/dev/null 2>&1 || true; cleanup' EXIT
for _ in $(seq 1 60); do
if curl -fsS "http://127.0.0.1:$CQ_MGR_PORT/health" >/dev/null 2>&1; then
break
fi
sleep 1
done
workdirs_json="$(curl -fsS "http://127.0.0.1:$CQ_MGR_PORT/api/workdirs")"
printf '%s\n' "$workdirs_json"
WORKDIRS_JSON="$workdirs_json" bun -e 'const body=JSON.parse(process.env.WORKDIRS_JSON||"{}"); if (body.ok !== true || !Array.isArray(body.workdirs) || !body.workdirs.some((item)=>item && item.path === "/workspace" && item.providerId === "D601" && item.executionMode === "default")) { console.error(JSON.stringify(body)); process.exit(1); }'
---
apiVersion: tekton.dev/v1
kind: Task