{"record":{"id":"f32a41dcab870cc9","repo":"Hmbown/CodeWhale","slug":"pipeline-expected-an-array-of-items","errorCode":null,"errorMessage":"pipeline(): expected an array of items","messagePattern":"pipeline\\(\\): expected an array of items","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"crates/workflow-js/src/vm.rs","lineNumber":1095,"sourceCode":"    }\n    return Promise.all(thunks.map((thunk) => {\n      try {\n        return Promise.resolve(typeof thunk === \"function\" ? thunk() : thunk).catch((err) => {\n          if (isFatalTaskError(err)) throw err;\n          hostLog(\"parallel(): dropped a failed slot as null: \" + String((err && err.message) || err));\n          return null;\n        });\n      } catch (err) {\n        if (isFatalTaskError(err)) return Promise.reject(err);\n        hostLog(\"parallel(): dropped a failed slot as null: \" + String((err && err.message) || err));\n        return null;\n      }\n    }));\n  };\n\n  globalThis.pipeline = (items, ...stages) => {\n    if (!Array.isArray(items)) {\n      throw new TypeError(\"pipeline(): expected an array of items\");\n    }\n    if (items.length > MAX_ITEMS) {\n      throw new Error(\"pipeline(): max \" + MAX_ITEMS + \" items per call\");\n    }\n    return Promise.all(items.map(async (item, index) => {\n      let value = item;\n      for (const stage of stages) {\n        try {\n          value = await stage(value, item, index);\n        } catch (err) {\n          if (isFatalTaskError(err)) throw err;\n          hostLog(\"pipeline(): dropped item \" + index + \" as null: \" + String((err && err.message) || err));\n          return null;\n        }\n      }\n      return value;\n    }));\n  };","sourceCodeStart":1077,"sourceCodeEnd":1113,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/workflow-js/src/vm.rs#L1077-L1113","documentation":"plugin_target_path computes the destination directory name from the plugin's name and runs it through validate_skill_name_segment (skills/install.rs:1536). A name is rejected if it is empty, has leading/trailing or any internal whitespace, equals '.' or '..', contains '/' or '\\\\', or is not a single normal path component. This is a path-traversal guard: the name becomes a directory under the user plugins dir, so it must be exactly one safe segment.","triggerScenarios":"Installing a plugin whose manifest [plugin].name is e.g. \"my plugin\" (space), \"../escape\", \"a/b\", \"\" or \"  padded  \"; plugin_target_path then refuses to build the target path.","commonSituations":"Hand-authored plugin.json with a human-readable display name in the name field; names copied from a docs headline with trailing whitespace; malicious or malformed third-party bundles attempting traversal; version differences where an older installer accepted names the current one rejects.","solutions":["Change the manifest name to a slug: lowercase letters, digits, and hyphens only, no spaces or path separators (e.g. \"my-plugin\").","Check for invisible characters: leading/trailing spaces, non-breaking spaces, or a BOM before the name in the JSON.","Keep a separate display/description field for the pretty name; the name field is a directory identifier."],"exampleFix":"// before (plugin.json)\n{ \"plugin\": { \"name\": \"My Fancy Plugin\", ... } }\n\n// after\n{ \"plugin\": { \"name\": \"my-fancy-plugin\", \"description\": \"My Fancy Plugin\" } }","handlingStrategy":"validation","validationCode":"let name = &manifest.plugin.name;\nlet safe = !name.is_empty()\n    && name.trim() == name\n    && !name.chars().any(char::is_whitespace)\n    && !name.contains('/') && !name.contains('\\\\')\n    && name != \".\" && name != \"..\";\nif !safe {\n    anyhow::bail!(\"choose a slug-style plugin name (letters, digits, hyphens)\");\n}","typeGuard":"fn is_safe_plugin_name(name: &str) -> bool {\n    !name.is_empty()\n        && name.trim() == name\n        && !name.chars().any(char::is_whitespace)\n        && !matches!(name, \".\" | \"..\")\n        && !name.contains('/') && !name.contains('\\\\')\n}","tryCatchPattern":"match plugin_target_path(&name, &plugins_dir) {\n    Ok(path) => path,\n    Err(e) if e.to_string().contains(\"not a safe directory name\") => {\n        let slug: String = name.trim().to_lowercase()\n            .chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }).collect();\n        plugin_target_path(slug.trim_matches('-'), &plugins_dir)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Author [plugin].name as a slug: lowercase, hyphens, no spaces or separators.","Put human-readable titles in a description field, never in name.","Add a CI lint that runs validate_skill_name_segment over published plugin names.","Watch for invisible whitespace and BOM characters when copying names from docs."],"tags":["rust","plugins","install","path-safety","validation"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}