{"record":{"id":"f75c0624b12bb8d6","repo":"Hmbown/CodeWhale","slug":"workspace-must-not-be-empty","errorCode":null,"errorMessage":"workspace must not be empty","messagePattern":"workspace must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7897,"sourceCode":"        if let Some(model) = req.model.as_ref()\n            && model.trim().is_empty()\n        {\n            bail!(\"model must not be empty\");\n        }\n        if let Some(mode) = req.mode.as_ref()\n            && mode.trim().is_empty()\n        {\n            bail!(\"mode must not be empty\");\n        }\n        if let Some(permission_posture) = req.permission_posture.as_ref()\n            && permission_posture.trim().is_empty()\n        {\n            bail!(\"permission_posture must not be empty\");\n        }\n        if let Some(workspace) = req.workspace.as_ref()\n            && workspace.as_os_str().is_empty()\n        {\n            bail!(\"workspace must not be empty\");\n        }\n\n        // Source resolution reads config files. Do it off the Tokio worker,\n        // then recheck the conversation identity before committing the grant.\n        let shell_policy_workspace = if req.allow_shell == Some(true) || req.workspace.is_some() {\n            let current = self.get_thread(id).await?;\n            if req.allow_shell.unwrap_or(current.allow_shell) {\n                let workspace = req.workspace.clone().unwrap_or(current.workspace);\n                self.validate_shell_access_policy(&workspace, config_path, config_profile)\n                    .await?;\n                Some(workspace)\n            } else {\n                None\n            }\n        } else {\n            None\n        };\n        let configured_sandbox_mode = self.read_config().sandbox_mode.clone();","sourceCodeStart":7879,"sourceCodeEnd":7915,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L7879-L7915","documentation":"When a thread-update request includes a `workspace`, the path must not be the empty path (`PathBuf::new()` / empty OsStr). The workspace anchors the thread's file access and shell policy, so an empty workspace would be meaningless or unsafe.","triggerScenarios":"Calling update-thread with `workspace: Some(PathBuf::new())`; a JSON body with `\"workspace\": \"\"` deserialized into an empty PathBuf.","commonSituations":"A client strips the workspace field to \"\" when clearing it; a path variable never got assigned before the call; deserializing an empty string from config into `Option<PathBuf>` yields Some(empty).","solutions":["Pass `None` for `workspace` to keep the current workspace.","Resolve the workspace to a real absolute directory path before the call.","On the client side, treat empty-string workspace as absent and omit the field."],"exampleFix":"// before\nUpdateThreadRequest { workspace: Some(PathBuf::from(ws_str)), .. } // ws_str may be \"\"\n\n// after\nUpdateThreadRequest {\n    workspace: if ws_str.trim().is_empty() { None } else { Some(PathBuf::from(ws_str)) },\n    ..\n}","handlingStrategy":"validation","validationCode":"if let Some(ws) = &req.workspace { if ws.as_os_str().is_empty() { req.workspace = None; } }","typeGuard":"fn valid_workspace(ws: &Option<PathBuf>) -> bool {\n    ws.as_ref().map_or(true, |p| !p.as_os_str().is_empty())\n}","tryCatchPattern":null,"preventionTips":["Resolve workspace paths to absolute existing directories before updates","Map empty-string workspaces to None when deserializing","Verify the directory exists (canonicalize) before assigning it to a thread"],"tags":["validation","workspace","path"],"backgroundTag":"invalid-argument-value","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}