{"record":{"id":"69554816524e1c4a","repo":"zeroclaw-labs/zeroclaw","slug":"tool-name-must-not-be-empty","errorCode":null,"errorMessage":"Tool name must not be empty","messagePattern":"Tool name must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-runtime/src/security/estop.rs","lineNumber":264,"sourceCode":"\n        Ok(())\n    }\n}\n\npub fn resolve_state_file_path(config_dir: &Path, state_file: &str) -> PathBuf {\n    let expanded = shellexpand::tilde(state_file).into_owned();\n    let path = PathBuf::from(expanded);\n    if path.is_absolute() {\n        path\n    } else {\n        config_dir.join(path)\n    }\n}\n\nfn normalize_tool_name(raw: &str) -> Result<String> {\n    let value = raw.trim().to_ascii_lowercase();\n    if value.is_empty() {\n        anyhow::bail!(\"Tool name must not be empty\");\n    }\n    if !value\n        .chars()\n        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')\n    {\n        anyhow::bail!(\"Tool name '{raw}' contains invalid characters\");\n    }\n    Ok(value)\n}\n\nfn dedup_sort(values: &[String]) -> Vec<String> {\n    let mut deduped = values\n        .iter()\n        .map(|value| value.trim())\n        .filter(|value| !value.is_empty())\n        .map(ToString::to_string)\n        .collect::<Vec<_>>();\n    deduped.sort_unstable();","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/estop.rs#L246-L282","documentation":"normalize_tool_name was given a string that is empty after trimming (or contains only whitespace). Tool names passed to estop engage/resume (e.g. ResumeSelector::Tools) must be non-empty so the frozen-tools list stays meaningful; an empty selector could never match anything and usually signals a caller bug.","triggerScenarios":"Calling engage() or resume(ResumeSelector::Tools(...)) with an empty string, a \"\", or a whitespace-only entry in the tools list; building the list from unvalidated config or user input.","commonSituations":"Config file with `tools = [\"\"]` placeholders; splitting a comma-separated string that has a trailing comma; forms/APIs that submit before the user types a tool name.","solutions":["Filter empty/whitespace entries out of the tools list before calling engage/resume.","Validate at the input boundary (config parse, API handler) that tool names are non-empty.","Use the same normalization rules as ZeroClaw (trim + lowercase + [a-z0-9_-]) when pre-validating.","Log which raw value was rejected so the source of the blank entry is findable."],"exampleFix":"// before\nestop.resume(ResumeSelector::Tools(vec![\"\".into(), \"fs_read\".into()]), None, None)?;\n\n// after — strip empties first\nlet tools = [\"fs_read\", \"web_search\"];\nassert!(tools.iter().all(|t| !t.trim().is_empty()));\nestop.resume(ResumeSelector::Tools(tools.to_vec()), None, None)?;","handlingStrategy":"validation","validationCode":"let tools: Vec<String> = raw_tools\n    .into_iter()\n    .map(|t| t.trim().to_ascii_lowercase())\n    .filter(|t| !t.is_empty())\n    .collect();\nassert!(!tools.is_empty(), \"tool selector list must contain at least one name\");","typeGuard":"fn is_valid_tool_name(raw: &str) -> bool {\n    let v = raw.trim().to_ascii_lowercase();\n    !v.is_empty() && v.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')\n}","tryCatchPattern":"Err(e) if e.to_string() == \"Tool name must not be empty\" => {\n    // filter blanks from the source list and retry; log the offending raw input\n}","preventionTips":["Normalize and filter tool lists at the input boundary (config parse, API handler).","Reject blank entries in forms/UI before submission.","Unit-test tool-name normalization paths with whitespace-only inputs."],"tags":["estop","validation","tool-name","input-validation"],"backgroundTag":"invalid-tool-name","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}