{"record":{"id":"afea1491c477e70b","repo":"astrid-runtime/astrid","slug":"label-is-not-a-canonical-cache-component","errorCode":null,"errorMessage":"{label} is not a canonical cache component","messagePattern":"(.+?) is not a canonical cache component","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/paths.rs","lineNumber":163,"sourceCode":"fn validate_cache_component(label: &str, value: &str, digest: bool) -> anyhow::Result<()> {\n    let valid = !value.is_empty()\n        && value.len() <= 128\n        && value\n            .bytes()\n            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))\n        && !value.starts_with('.')\n        && !value.ends_with('.')\n        && !value.contains(\"..\")\n        && (!digest\n            || (value.len() == 64\n                && value\n                    .bytes()\n                    .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))))\n        && Path::new(value)\n            .components()\n            .all(|component| matches!(component, std::path::Component::Normal(_)));\n    if !valid {\n        anyhow::bail!(\"{label} is not a canonical cache component\")\n    }\n    Ok(())\n}\n\n/// Remove every disposable user capsule materialization from the runtime\n/// cache after validating the complete tree without following redirects.\n///\n/// Durable packages are never touched. A fresh materialization is created\n/// from a verified storage snapshot when needed, so deleting stale or\n/// interrupted cache generations at boot is safe and avoids reusing an\n/// unverified crash residue.\npub fn clear_capsule_materialization_cache(home: &AstridHome) -> anyhow::Result<()> {\n    let root = home.run_dir().join(\"capsules\");\n    let metadata = match std::fs::symlink_metadata(&root) {\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(error) => return Err(error).context(\"inspect capsule materialization cache\"),\n    };","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/paths.rs#L145-L181","documentation":"validate_cache_component asserts that a path segment used to build a cache location (e.g. a capsule name or version component) is canonical: only lowercase hex/digit-safe characters as checked by the preceding validation, and every std::path::Component is Normal — no '.', '..', prefixes, or separators. Any component that could escape or alias the cache tree is rejected with this error before any path is constructed.","triggerScenarios":"Calling resolve_cache_target_dir with a capsule name/version/label containing path separators, '..', leading dots, or non-lowercase-hex characters where a hex-digest form is required, so the `valid` conjunction evaluates false.","commonSituations":"Capsule ids with uppercase or ':', '/', '\\\\' characters; passing an unnormalized version like '1.0.0-beta+build' or a raw user string; on Windows, drive-letter components; an empty string component.","solutions":["Sanitize the component: lowercase it and strip/encode characters outside [0-9a-f] (or the allowed set) before passing it","Reject or hash non-conforming identifiers — e.g. use a hex digest of the capsule name instead of the raw name","Trim path separators and reject any input containing '/', '\\\\', '..', or '.' segments at the API boundary","Log the offending label and value to identify which caller passed a non-canonical component"],"exampleFix":"// before\nlet dir = resolve_cache_target_dir(&cache_root, capsule_name)?; // may contain ':' or '/'\n// after\nlet component: String = capsule_name.bytes().filter(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f')).map(|b| b as char).collect();\nlet dir = resolve_cache_target_dir(&cache_root, &component)?;","handlingStrategy":"validation","validationCode":"fn canonical_cache_component(value: &str) -> bool {\n    !value.is_empty()\n        && value.bytes().all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f'))\n        && std::path::Path::new(value).components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","typeGuard":"fn as_cache_component(s: &str) -> Option<&str> {\n    canonical_cache_component(s).then_some(s)\n}","tryCatchPattern":"match resolve_cache_target_dir(&root, comp) {\n    Err(e) if e.to_string().contains(\"not a canonical cache component\") => {\n        // hash or sanitize the identifier and retry\n    }\n    other => other?,\n}","preventionTips":["Hash raw identifiers into hex digests before using them as cache path components","Reject user input containing '/', '\\\\', '..', or non-lowercase-hex chars at the boundary","Lowercase and normalize ids when constructing cache keys","Test the path-builder with adversarial inputs ('..', absolute paths, empty string)"],"tags":["cache","path-validation","security"],"backgroundTag":"invalid-argument-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}