{"record":{"id":"ca8961ad6ed492b5","repo":"Hmbown/CodeWhale","slug":"kind-must-be-a-single-path-component","errorCode":null,"errorMessage":"{kind} must be a single path component","messagePattern":"(.+?) must be a single path component","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":1837,"sourceCode":"    let run: AutomationRunRecord = serde_json::from_str(&raw)\n        .with_context(|| format!(\"Failed to parse {}\", path.display()))?;\n    if run.schema_version > CURRENT_RUN_SCHEMA_VERSION {\n        bail!(\n            \"Automation run schema v{} is newer than supported v{}\",\n            run.schema_version,\n            CURRENT_RUN_SCHEMA_VERSION\n        );\n    }\n    Ok(run)\n}\n\nfn ensure_safe_storage_id(kind: &str, value: &str) -> Result<()> {\n    let mut components = Path::new(value).components();\n    let Some(component) = components.next() else {\n        bail!(\"{kind} must not be empty\");\n    };\n    if components.next().is_some() || !matches!(component, std::path::Component::Normal(_)) {\n        bail!(\"{kind} must be a single path component\");\n    }\n    Ok(())\n}\n\nfn validate_name_and_prompt(name: &str, prompt: &str) -> Result<()> {\n    if name.trim().is_empty() {\n        bail!(\"Automation name is required\");\n    }\n    if prompt.trim().is_empty() {\n        bail!(\"Automation prompt is required\");\n    }\n    Ok(())\n}\n\nfn normalize_optional_string(value: Option<String>) -> Option<String> {\n    value\n        .map(|value| value.trim().to_string())\n        .filter(|value| !value.is_empty())","sourceCodeStart":1819,"sourceCodeEnd":1855,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/automation_manager.rs#L1819-L1855","documentation":"The second ensure_safe_storage_id check requires the id to be exactly one Normal path component. It rejects ids containing '/', parent segments ('..'), current-directory segments ('.'), absolute prefixes, and any other multi-component or non-normal path shape. This is the path-traversal guard that keeps storage writes inside the automation/task directories.","triggerScenarios":"Passing an id like 'a/b', '../secrets', '.', '..', or '/etc/passwd' where an automation id, trigger id, run id, or task id is expected; ids assembled by joining user input with '/'.","commonSituations":"Reusing a file path or URL slug as the storage id; concatenating team/name into an id with separators; ids built from free-form model or task names that contain slashes.","solutions":["Use a flat, opaque id (uuid or slugified name) instead of a path-like string.","Sanitize before saving: replace '/' and other separators with '-' and strip leading dots.","Enforce an id pattern such as ^[A-Za-z0-9_-]+$ at the input boundary."],"exampleFix":"// before\nlet automation_id = format!(\"{}/{}\", team, name); // contains '/' -> bails\n\n// after\nlet automation_id = format!(\"{}-{}\", team, name)\n    .chars()\n    .map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '-' })\n    .collect::<String>();","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\n\nfn is_safe_storage_id(id: &str) -> bool {\n    let mut components = Path::new(id).components();\n    matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never accept raw paths or URLs as storage ids.","Slugify human-readable names before persisting them as ids.","Add a unit test asserting generated ids pass the single-component check."],"tags":["rust","path-traversal","validation","storage-id","security"],"backgroundTag":"path-validation-failed","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}