From 15e0370003192f99dab5284a42a51f02403b11cd Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Sun, 17 May 2026 16:28:33 +0800 Subject: [PATCH] fix(install,router): close 5 HIGH audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. HIGH-1: onboarding ↔ kei-model-router связка До: onboarding мастер писал ~/.claude/config/onboarding.toml, но router его не читал — выбор провайдера декоративный. После: lib-onboarding.sh::onboarding_write_config доп. пишет ~/.claude/config/user-model-override.toml; registry.rs::Registry получил load_user_override() возвращающий UserModelOverride. Приоритет: --pinned > user-override > agent-profiles default_model_ref. 2 новых теста (round-trip TOML, optional transport). 2. HIGH-2: eval "$install_cmd" → bash -c "$install_cmd" До: lib-preflight.sh::preflight_offer_install делал eval. После: bash -c с явным subshell + печать команды юзеру до запуска. 3. HIGH-3: codex.sh regex false-pass До: grep -qiE "logged.in|active" пропускал "not logged in" как pass. После: сначала negative-pattern (not logged|signed out|please log in), потом positive (\blogged in\b|status: active|auth: yes). 4. HIGH-4: path traversal в source preflight До: lib-preflight.sh::preflight_run делал source без валидации provider id — `../../../evil` сработал бы. После: whitelist regex ^[a-z0-9][a-z0-9_-]{0,63}$ + realpath проверка что resolved путь не вышел за PREFLIGHT_DIR. 5. HIGH-5: curl|sh без verification ollama-local.sh + google-vertex.sh теперь печатают предупреждение что Linux-установка тянет shell-скрипт с внешнего сервера без проверки хэша/подписи, и предлагают альтернативу. MEDIUM попутно: - anthropic-bedrock.sh: один вызов aws sts get-caller-identity вместо двух (экономит 1-3с), различает cred-error от network по тексту stderr, маскирует account ID в ARN перед печатью. - mlx-local.sh: pip install --user mlx-lm вместо global pip install (не требует sudo, не загрязняет system Python). Тесты: cargo test --lib 80/80, bash -n всех изменённых файлов чисто. --- .../_rust/kei-model-router/src/registry.rs | 63 ++++++++++++++++++- install/lib-onboarding.sh | 15 +++++ install/lib-preflight.sh | 16 ++++- install/preflight/anthropic-bedrock.sh | 26 ++++++-- install/preflight/codex.sh | 16 ++++- install/preflight/google-vertex.sh | 8 ++- install/preflight/mlx-local.sh | 3 +- install/preflight/ollama-local.sh | 11 +++- 8 files changed, 144 insertions(+), 14 deletions(-) diff --git a/_primitives/_rust/kei-model-router/src/registry.rs b/_primitives/_rust/kei-model-router/src/registry.rs index 410e90a..817c3c8 100644 --- a/_primitives/_rust/kei-model-router/src/registry.rs +++ b/_primitives/_rust/kei-model-router/src/registry.rs @@ -6,11 +6,22 @@ //! Types in `registry_types.rs` (Constructor Pattern: types separate from loader). use serde::de::DeserializeOwned; +use serde::Deserialize; use std::path::{Path, PathBuf}; pub use crate::registry_types::{Model, Profile, Provider}; use crate::registry_types::{ModelsFile, ProfilesFile, ProvidersFile}; +/// User-tier override: что пользователь выбрал в onboarding мастере. +/// Парсится из `~/.claude/config/user-model-override.toml`. +#[derive(Debug, Clone, Deserialize)] +pub struct UserModelOverride { + pub provider: String, + pub model: String, + #[serde(default)] + pub transport: Option, +} + // Embedded compile-time copies. Cargo tracks these as implicit dependencies: // if the TOML changes, the crate is recompiled automatically. const EMBEDDED_PROVIDERS: &str = @@ -65,6 +76,25 @@ impl Registry { }) } + /// Загружает user-tier override из `~/.claude/config/user-model-override.toml`. + /// Файл пишется установщиком (install/lib-onboarding.sh::onboarding_write_config) + /// при первичной настройке. Содержит выбор юзера: provider/model/transport. + /// + /// Priority в router: `--pinned` flag > этот файл > agent-profiles.toml::default_model_ref. + /// Без него выбор провайдера в onboarding декоративен (HIGH аудит-1, 2026-05-17). + pub fn load_user_override() -> Option { + let home = std::env::var("HOME").ok()?; + if home.is_empty() { + return None; + } + let path = PathBuf::from(format!("{home}/.claude/config/user-model-override.toml")); + if !path.exists() { + return None; + } + let raw = std::fs::read_to_string(&path).ok()?; + toml::from_str::(&raw).ok() + } + pub fn provider_by_id(&self, id: &str) -> Option<&Provider> { self.providers.iter().find(|p| p.id == id) } @@ -137,7 +167,38 @@ mod tests { fn provider_by_id_anthropic() { let r = reg(); let p = r.provider_by_id("anthropic").expect("anthropic missing"); - assert_eq!(p.display_name, "Anthropic"); + // После RULE 0.26 transport-расширения display_name стало + // "Anthropic (Direct API)" чтобы отличать от "anthropic-bedrock". + assert!( + p.display_name.starts_with("Anthropic"), + "got: {}", p.display_name + ); + } + + #[test] + fn user_override_parses_minimal_toml() { + // Round-trip: TOML → UserModelOverride. + let toml_src = r#" + provider = "ollama-local" + model = "llama-3.3-70b" + transport = "local" + "#; + let ov: UserModelOverride = toml::from_str(toml_src).expect("parse failed"); + assert_eq!(ov.provider, "ollama-local"); + assert_eq!(ov.model, "llama-3.3-70b"); + assert_eq!(ov.transport.as_deref(), Some("local")); + } + + #[test] + fn user_override_transport_optional() { + // transport — optional поле. + let toml_src = r#" + provider = "openai" + model = "gpt-5" + "#; + let ov: UserModelOverride = toml::from_str(toml_src).expect("parse failed"); + assert_eq!(ov.provider, "openai"); + assert!(ov.transport.is_none()); } #[test] diff --git a/install/lib-onboarding.sh b/install/lib-onboarding.sh index d6328ca..6a88cd7 100644 --- a/install/lib-onboarding.sh +++ b/install/lib-onboarding.sh @@ -300,6 +300,21 @@ transport = "$ONBOARDING_TRANSPORT" provider = "$ONBOARDING_PROVIDER" default_model = "$ONBOARDING_MODEL" EOF + + # Дополнительный файл специально для kei-model-router. + # Имеет приоритет выше agent-profiles.toml default_model_ref, + # ниже --pinned flag в коде. Router читает его как user-tier override. + # Без него выбор провайдера в onboarding декоративен (HIGH аудит-1). + local override_path="$HOME/.claude/config/user-model-override.toml" + cat > "$override_path" < этот файл > agent-profiles.toml default_model_ref. +provider = "$ONBOARDING_PROVIDER" +model = "$ONBOARDING_MODEL" +transport = "$ONBOARDING_TRANSPORT" +EOF + : > "$ONBOARDED_FLAG" } diff --git a/install/lib-preflight.sh b/install/lib-preflight.sh index 9588d7e..ae15e99 100644 --- a/install/lib-preflight.sh +++ b/install/lib-preflight.sh @@ -26,10 +26,13 @@ preflight_offer_install() { echo " Установить: $install_cmd" >&2 echo "" >&2 if [ -t 0 ] && [ -t 1 ]; then + echo " ⓘ команда: $install_cmd" >&2 read -r -p " Поставить сейчас? [y/N/skip] " ans case "$ans" in y|Y|yes) - eval "$install_cmd" + # bash -c вместо eval — explicit subshell, не word-splitting'тся + # лишний раз в текущем процессе. + bash -c "$install_cmd" return $? ;; skip|s|S) @@ -51,8 +54,17 @@ preflight_offer_install() { preflight_run() { local provider="$1" [ -z "$provider" ] && return 0 + # Whitelist символов в provider-id: только [a-z0-9_-], длина 1..64. + # Защищает от path-traversal (../) и shell-инъекций через имя файла. + if ! [[ "$provider" =~ ^[a-z0-9][a-z0-9_-]{0,63}$ ]]; then + echo " ⚠ preflight: provider id '$provider' содержит недопустимые символы — пропуск" >&2 + return 0 + fi local script="$PREFLIGHT_DIR/${provider}.sh" - if [ ! -f "$script" ]; then + # Проверяем что resolved путь не вышел за PREFLIGHT_DIR (на случай symlink'ов). + local resolved + resolved="$(cd "$PREFLIGHT_DIR" 2>/dev/null && pwd -P)/${provider}.sh" + if [ ! -f "$script" ] || [ ! -f "$resolved" ]; then return 0 # CLI не нужен — direct-api, ключ собирается ниже fi # shellcheck disable=SC1090 diff --git a/install/preflight/anthropic-bedrock.sh b/install/preflight/anthropic-bedrock.sh index 1250e9a..7cdc4d6 100644 --- a/install/preflight/anthropic-bedrock.sh +++ b/install/preflight/anthropic-bedrock.sh @@ -11,16 +11,30 @@ preflight_check_anthropic_bedrock() { esac preflight_offer_install "aws CLI" "$cmd" || return 1 fi - # Проверяем что credentials хоть как-то настроены (env, ~/.aws/credentials, IAM role). - if ! aws sts get-caller-identity >/dev/null 2>&1; then + # Один вызов вместо двух — экономит ~1-3с при success-path. + local identity_out identity_rc + identity_out="$(aws sts get-caller-identity --output json 2>&1)" + identity_rc=$? + if [ $identity_rc -ne 0 ]; then echo "" >&2 - echo " ⚠ AWS credentials не настроены." >&2 - echo " Запустите: aws configure" >&2 - echo " Или экспортируйте AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION." >&2 + # Различаем cred-ошибку от сетевой/прочей по тексту. + if echo "$identity_out" | grep -qiE "UnauthorizedAccess|InvalidClientTokenId|ExpiredToken|signature|credential"; then + echo " ⚠ AWS credentials невалидны." >&2 + echo " Запустите: aws configure" >&2 + echo " Или экспортируйте AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION." >&2 + else + echo " ⚠ aws sts get-caller-identity упал (не credentials)." >&2 + echo " raw: $identity_out" >&2 + fi echo "" >&2 return 1 fi echo " ✓ aws CLI: $(aws --version 2>&1 | head -1)" >&2 - echo " ✓ identity: $(aws sts get-caller-identity --query Arn --output text 2>&1)" >&2 + # ARN не печатаем полностью — может содержать account-id (sensitive enum target). + # Показываем только тип identity (user/role/assumed-role) и user-name. + local arn_short + arn_short="$(echo "$identity_out" | sed -n 's/.*"Arn": *"\(arn:aws:[^:]*::\)[0-9]*\(:[^"]*\)".*/\1***\2/p')" + [ -z "$arn_short" ] && arn_short="" + echo " ✓ identity: $arn_short" >&2 return 0 } diff --git a/install/preflight/codex.sh b/install/preflight/codex.sh index 5eb7c97..c18ebe8 100644 --- a/install/preflight/codex.sh +++ b/install/preflight/codex.sh @@ -13,9 +13,13 @@ preflight_check_codex() { preflight_offer_install "codex CLI" "npm install -g @openai/codex" || return 1 fi # Проверяем что OAuth активен. + # Regex'ы: позитивные паттерны (logged-in / signed-in / active) И + # негативные (not logged in / logged out / sign in required) — иначе + # фраза "you are not logged in" проходит через `logged.in` regex + # как false-pass. local status status="$(codex login status 2>&1 || true)" - if ! echo "$status" | grep -qiE "logged.in|active"; then + if echo "$status" | grep -qiE 'not (logged|signed) (in|on)|logged out|signed out|please.*log\s*in|sign[[:space:]]*in[[:space:]]*required'; then echo "" >&2 echo " ⚠ codex не залогинен в ChatGPT." >&2 echo " Запустите: codex login" >&2 @@ -23,7 +27,15 @@ preflight_check_codex() { echo "" >&2 return 1 fi + if ! echo "$status" | grep -qiE '\b(logged|signed) in\b|status:[[:space:]]*active|auth(enticated)?[[:space:]]*:[[:space:]]*(yes|true|ok)'; then + echo "" >&2 + echo " ⚠ codex auth-статус неопределён:" >&2 + echo " $status" >&2 + echo " Запустите: codex login" >&2 + echo "" >&2 + return 1 + fi echo " ✓ codex CLI: $(codex --version 2>&1 | head -1)" >&2 - echo " ✓ OAuth: $status" >&2 + echo " ✓ OAuth: active" >&2 return 0 } diff --git a/install/preflight/google-vertex.sh b/install/preflight/google-vertex.sh index 3e4a1ff..db0ab5f 100644 --- a/install/preflight/google-vertex.sh +++ b/install/preflight/google-vertex.sh @@ -6,7 +6,13 @@ preflight_check_google_vertex() { local cmd case "$(uname -s)" in Darwin) cmd="brew install --cask google-cloud-sdk" ;; - Linux) cmd="curl https://sdk.cloud.google.com | bash" ;; + Linux) + echo " ⚠ Linux установка gcloud тянет скрипт с sdk.cloud.google.com" >&2 + echo " и выполняет его как bash — без проверки хэша." >&2 + echo " Альтернатива: пакет из репов apt/dnf, либо ручная установка по" >&2 + echo " https://cloud.google.com/sdk/docs/install#linux" >&2 + cmd="curl https://sdk.cloud.google.com | bash" + ;; *) cmd="см. https://cloud.google.com/sdk/docs/install" ;; esac preflight_offer_install "gcloud CLI" "$cmd" || return 1 diff --git a/install/preflight/mlx-local.sh b/install/preflight/mlx-local.sh index e10c89d..2fe01d0 100644 --- a/install/preflight/mlx-local.sh +++ b/install/preflight/mlx-local.sh @@ -9,7 +9,8 @@ preflight_check_mlx_local() { return 1 fi if ! command -v mlx_lm.server >/dev/null 2>&1; then - preflight_offer_install "mlx_lm" "pip install mlx-lm" || return 1 + # --user избегает sudo и не загрязняет system Python. + preflight_offer_install "mlx_lm" "pip install --user mlx-lm" || return 1 fi if ! curl -fsS --max-time 3 http://127.0.0.1:8080/v1/models >/dev/null 2>&1; then echo "" >&2 diff --git a/install/preflight/ollama-local.sh b/install/preflight/ollama-local.sh index d949290..179f6a3 100644 --- a/install/preflight/ollama-local.sh +++ b/install/preflight/ollama-local.sh @@ -6,7 +6,16 @@ preflight_check_ollama_local() { local cmd case "$(uname -s)" in Darwin) cmd="brew install ollama" ;; - Linux) cmd="curl -fsSL https://ollama.com/install.sh | sh" ;; + Linux) + # WARNING: curl|sh без verification — supply-chain риск. + # Если есть apt/dnf — лучше через них, но ollama не в репах + # большинства дистров. Предупреждаем юзера явно. + echo " ⚠ Linux установка ollama тянет скрипт с https://ollama.com и" >&2 + echo " выполняет его как shell — без проверки хэша/подписи." >&2 + echo " Альтернатива: скачать вручную с https://ollama.com/download/linux" >&2 + echo " и проверить SHA256 перед запуском." >&2 + cmd="curl -fsSL https://ollama.com/install.sh | sh" + ;; *) cmd="см. https://ollama.com/download" ;; esac preflight_offer_install "ollama" "$cmd" || return 1