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-пути). HelperinitSSRFGuard(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 primitiveNewSafeHTTPClientFromGetterсам по себе fail-closed (dial возвращаетSSRF guard not configured), но panic в production-caller’е — правильная реакция на нарушение инварианта package’а.