{"record":{"id":"cb7be02eb49083ae","repo":"Hmbown/CodeWhale","slug":"label-contains-unsupported-characters","errorCode":null,"errorMessage":"{label} contains unsupported characters","messagePattern":"(.+?) contains unsupported characters","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":306,"sourceCode":"            out.push_str(tail);\n        }\n    }\n    out\n}\n\nfn validated_record_id<'a>(id: &'a str, label: &str) -> Result<&'a str> {\n    let trimmed = id.trim();\n    if trimmed.is_empty() {\n        bail!(\"{label} cannot be empty\");\n    }\n    if trimmed != id {\n        bail!(\"{label} cannot contain leading or trailing whitespace\");\n    }\n    if !trimmed\n        .chars()\n        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')\n    {\n        bail!(\"{label} contains unsupported characters\");\n    }\n    Ok(trimmed)\n}\n\nfn agent_mail_workspace_id(workspace: &Path) -> Result<String> {\n    let canonical = workspace\n        .canonicalize()\n        .with_context(|| format!(\"resolve Agent Mail workspace {}\", workspace.display()))?;\n    let digest = Sha256::digest(canonical.to_string_lossy().as_bytes());\n    let digest = digest\n        .iter()\n        .map(|byte| format!(\"{byte:02x}\"))\n        .collect::<String>();\n    Ok(format!(\"ws_{digest}\"))\n}\n\nfn agent_mail_sender_identity(thread: &ThreadRecord) -> Result<String> {\n    thread","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L288-L324","documentation":"`validated_record_id` restricts record ids to ASCII alphanumeric characters plus `-` and `_`, because ids are used as file names under the threads store. Any other character (slash, dot, space, unicode) is rejected — this also blocks path traversal.","triggerScenarios":"Calling any threads-store API (`record_path`, `save_turn`, `save_item`, list functions) with an id containing characters outside `[A-Za-z0-9_-]`.","commonSituations":"Using an email, URL, or file path as a thread id; ids built from user names with spaces or dots; malicious `../` input being safely rejected.","solutions":["Slugify the id to the allowed charset before storing.","Use a generated opaque id (UUID/ULID) instead of a human string.","Keep a mapping from the human-readable label to a safe stored id."],"exampleFix":"// before\nlet thread_id = format!(\"{}'s session\", user.email); // contains @ and .\nstore.save_turn(&thread_id, &turn_id, record)?;\n// after\nlet thread_id = uuid::Uuid::new_v4().to_string();\nstore.save_turn(&thread_id, &turn_id, record)?;","handlingStrategy":"validation","validationCode":"fn id_charset_ok(id: &str) -> bool { !id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')) }","typeGuard":"fn valid_record_id(id: &str) -> Option<&str> { id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')).then_some(id) }","tryCatchPattern":"match store.save_turn(thread_id, turn_id, record) { Err(e) if e.contains(\"unsupported characters\") => { let safe = slugify(turn_id); retry_with(safe) }, Err(e) => return Err(e.into()), Ok(v) => v }","preventionTips":["Use generated UUID/ULID ids instead of human strings","Slugify any human-derived label before storing","Never use emails, URLs, or paths as record ids"],"tags":["validation","path-safety","storage"],"backgroundTag":"invalid-identifier-format","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"}