0.136.4

Fixed

  • **SSRF guard инициализируется до провайдеров **: в cmd/taigaclaw/main.go вызов SetSSRFGuard шёл в самом конце инициализации — после healthChecker.Start, initEmbedder, initReranker, NewProviderHealthChecker и создания Consolidator/Dream/AutoCompact. Все эти компоненты делали snapshot guard’а (через ssrfGuard.Load()) в момент своего создания и навсегда оставались на init()-default (NewSSRFGuard(nil), блокирует все приватные IP). В результате классификатор стратегии падал по SSRF на oneshot-вызове к LiteLLM на приватном IP (strategy router failed, defaulting to react), а consolidator / dream / autocompact / health-checker / embedder / reranker регулярно получали blocked internal/private IP. Перезапуск не помогал — порядок инициализации тот же. Фронт-энд работал (react-цикл использовал per-request guard от registerToolsForAgent, ), но любая фоновая LLM-работа падала.

Changed

  • Dynamic guard-getter в internal/security/ssrf.go: safeTransport теперь принимает GuardGetter (func *SSRFGuard) вместо snapshot — DialContext/DialTLSContext дёргают геттер на каждом коннекте, так что swap через SetSSRFGuard виден на следующем dial без пересоздания клиента. NewSafeHTTPClientFromGetter(getter) — публичный API для клиентов с dynamic-resolver. Nil-getter fail-closed: dial возвращает SSRF guard not configured. Закрывает класс snapshot-staleness регрессий навсегда — баг порядка инициализации больше не воспроизводится, даже если кто-то случайно вызовет SetSSRFGuard поздно.
  • Перенос initSSRFGuard + providers.SetSSRFGuard в начало main.go: теперь guard строится сразу после store+logger, ДО создания любого провайдера. agentLoop.SetSSRFGuard тоже перенесён выше — до connProxy.SetSSRFGuard(agentLoop.SSRFGuard()) (раньше proxy получал пустой default-guard, ещё один SSRF-leak для proxy-пути). Helper initSSRFGuard(store) вынесен рядом с другими init-функциями.
  • **Per-request SSRFGuard в OneShotExtract **: providers.OneShotExtract теперь принимает opts ...OneShotOption, включая WithAgentSSRFGuard(*security.SSRFGuard). Опция прокидывает guard в ChatRequest.SSRFGuard, который читают OpenAICompatProvider.perRequestSSRFOpts / AnthropicProvider.perRequestSSRFOpts. RouteStrategyWithStore и RouteStrategy получили параметр ssrfGuard *security.SSRFGuard; actor.go:670 передаёт agentSSRFGuard (per-agent guard уже в scope от registerToolsForAgent). Классификатор стратегии теперь honour’ит per-agent whitelist, а не падает на global guard. Совместимость со старыми callers (soul.go, profile.go) сохранена — opts-параметр вариативный.
  • Кеш HTTP-клиентов в providers/ssrf.go: loopbackClient (built once at init) и globalCloudClient (atomic.Pointer, обновляется в SetSSRFGuard) — убирают per-Chat аллокацию transport/dialer/connection-pool на hot path. Callers, мутирующие .Timeout (context_window.go, model_list.go, test.go), переведены на shallow-copy (c := *client; c.Timeout = X), чтобы не гонять с другими goroutine’ами. Тест TestSafeHTTPClientForSpec_CallersMustShallowCopyBeforeMutating кодирует контракт. Перфоманс-регрессия от dynamic-getter устранена: 99% чатов идут через кешированный global client (агенты без ip_whitelist); для агентов с ip_whitelist клиент строится per-call (одна аллокация на inbound-сообщение — пренебрежимо по сравнению с LLM round-trip).
  • SetSSRFGuard теперь re-callable: контракт расслаблен с «MUST be called exactly once» до «safe to call at any time» — dynamic-getter гарантирует что swap виден без rebuild. Документация переписана, чтобы не противоречить самой себе (раньше в одном комментарии говорилось «once at startup» и «late call is visible»). Best practice прежний: вызвать как можно раньше в main.go.

Security

  • Defensive panic на nil getter в safeHTTPClientForGuard: программная ошибка caller’а (передача nil getter’а) падает loudly вместо silent fail-open. Тест TestSafeHTTPClientForGuard_PanicsOnNilGetter кодирует инвариант. Security primitive NewSafeHTTPClientFromGetter сам по себе fail-closed (dial возвращает SSRF guard not configured), но panic в production-caller’е — правильная реакция на нарушение инварианта package’а.