Веб-исследования агентов (Web Research)
#1. Цель
Дать агентам Temporality полноценный доступ к внешним знаниям: поиск, чтение страниц, просмотр изображений и интерактивное исследование сайтов — с сохранением принципов наблюдаемости (каждое действие — событие в журнале), гейтов возможностей (capabilities) и сетевой безопасности (SSRF-защита).
Развитие идёт тремя фазами:
| Фаза | Содержание | Статус |
|---|---|---|
| 1 | web_fetch + web_search (self-hosted SearXNG) |
реализовано |
| 2 | Vision: images как content-блоки, view_image / fetch_image |
реализовано |
| 3 | Headless-браузер за CDP-контрактом (Lightpanda) | реализовано |
#2. Фаза 1 — web_fetch и web_search (реализовано)
#2.1. web_fetch
Загружает http(s)-страницу и возвращает агенту чистый markdown.
Контракт:
- аргументы:
url(обязательный, абсолютный http(s)),max_bytes(опционально, бюджет выдачи; default 48 KiB, max 128 KiB); - HTML → readability-извлечение статьи → markdown; если статья не извлеклась — конвертация всей страницы; последний fallback — plain text;
application/json,text/plain,text/markdown,xml,csv— как есть;- бинарное содержимое — NonRetryable-ошибка с советом скачать через
run_command+ curl в/workspace; - редиректы — до 5, финальный URL отражается в выдаче;
- формат результата:
url:/content-type:/title:+ тело, при усечении —[truncated at N of M bytes].
#2.2. web_search
Метапоиск через self-hosted SearXNG (решение: свой инстанс — без внешних поисковых API, ключей и лимитов; JSON-формат включён в searxng-settings.yml).
Контракт:
- аргументы:
query(обязательный),max_results(default 8, max 20); - GET
{KERNEL_SEARXNG_URL}/search?format=json&q=...; - в выдаче: instant answers, infoboxes, результаты (title + URL + snippet) markdown-списком;
- пустая выдача — явная подсказка переформулировать запрос;
- SearXNG не настроен (пустой
KERNEL_SEARXNG_URL) — тул не регистрируется,web_fetchпродолжает работать.
#2.3. Гейты и безопасность
- Оба тула регистрируются только при
network_accessрана (capabilitynetwork/ флаг агента). Без сети — тул вообще не попадает в список модели. - Авто-апрув по политике песочницы (
sandbox.workspace.v1): при включённой сети песочница и так может сделатьcurlна тот же адрес; наши тулы строго безопаснее — SSRF-фильтр, капы размеров, без exfil-заголовков. - SSRF-guard (
webTransport): резолв хоста → проверка всех IP против blocked-сетей (loopback, RFC1918, CGNAT, link-local, multicast, IPv6-аналоги) → диал разрешённого IP напрямую. Защищает и от DNS-rebinding: адрес не перерезолвится между проверкой и коннектом. Редиректы идут через тот же диалер. KERNEL_WEB_FETCH_ALLOW_PRIVATE=trueотключает guard для web_fetch — для внутренних док-серверов. По умолчанию выключен.- SearXNG-эндпоинт — конфигурация оператора (часто внутренний адрес docker-сети), поэтому SSRF-guard на него не распространяется: он защищает цели, выбранные моделью.
#2.4. Конфигурация
| Переменная | Значение | Default |
|---|---|---|
KERNEL_SEARXNG_URL |
базовый URL SearXNG | http://searxng:8080 (compose), пусто — web_search выключен |
KERNEL_WEB_FETCH_ALLOW_PRIVATE |
разрешить web_fetch в приватные сети | пусто (выключено) |
KERNEL_WEB_SEARCH_PER_MIN |
глобальный бюджет web_search на процесс ядра в минуту | 30 (0 — без лимита) |
Docker-compose поднимает сервис searxng (порт хоста 127.0.0.1:8889 — только для инспекции оператором).
Внутренние параметры: кап загрузки 2 MiB, таймауты 30s/15s, кэш выдачи 10 минут / 64 записи в рамках процесса ядра.
#2.5. Нагрузка, баны поисковиков и лимиты
Self-hosted SearXNG — это метапоиск: один запрос агента веером расходится по десяткам движков, и поисковые системы (особенно Google) банят не агента, а IP сервера капчами. Защита строится слоями:
- Разнообразие движков. SearXNG с
use_default_settings: trueопрашивает весь дефолтный набор (DuckDGo, Brave, Qwant, Bing, Mojeek, Wikipedia, ...) параллельно и мержит результаты — нагрузка на каждый отдельный движок остаётся низкой. Движок, начавший отвечать капчами, автоматически приостанавливается, выдача продолжает собираться из остальных. - Глобальный лимит на стороне ядра.
KERNEL_WEB_SEARCH_PER_MIN(default- — скользящее окно на весь процесс ядра, не на ран и не на проект:
runaway-цикл одного агента не может исчерпать чужой IP. Превышение —
NonRetryable-ошибка
SearchRateLimitedс подсказкой, через сколько секунд можно повторить.
- — скользящее окно на весь процесс ядра, не на ран и не на проект:
runaway-цикл одного агента не может исчерпать чужой IP. Превышение —
NonRetryable-ошибка
- Кэш одинаковых запросов. Идентичные (query, max_results) в течение 5 минут обслуживаются из кэша и не тратят бюджет лимита.
- Прокси для масштабирования. При крупном флоте агентов — исходящий
трафик движков через ротацию прокси (секция
outgoing.proxiesв настройках SearXNG).
#3. Фаза 2 — Vision (реализовано)
Мультимодальные модели потребляют изображения как content-блоки. OpenAI-совместимый API принимает image-части только в user-сообщениях — не в tool-результатах. Поэтому контракт разделён: тул возвращает в content только метаданные, а блоб — в ToolResult.Images; workflow после того, как все tool-ответы хода легли в историю (делегаты включительно), инжектирует блоки одним следующим user-сообщением («Image(s) produced by this turn's tool calls…»). Инжекция append-only — совместима с prompt-cache; дедуп-повтор вызова не ре-инжектирует (блок уже в истории).
#3.1. view_image
- аргументы:
path(workspace-относительный, ведущий/workspace/принимается); - регистрируется вместе с файловыми тулами при настроенной песочнице — чтение из изолированного workspace с тем же path-traversal-гейтом (
resolveToolTarget); - классифицируется как read-only (
readOnlyTools): падение тула не требует reconciliation.
#3.2. fetch_image
- аргументы:
url(абсолютный http(s)); - регистрируется при
network_accessвместе с web-тулами и разделяет их авто-апрув (sandbox.workspace.v1→ событиеapproval.auto_granted); - SSRF-guard как у web_fetch (
webTransport+KERNEL_WEB_FETCH_ALLOW_PRIVATE), редиректы ≤ 5, таймаут web_fetch; text/html→ NonRetryableUnsupportedContentс указанием использоватьweb_fetch.
#3.3. Пайплайн подготовки блока (prepareImageBlock)
- кап источника 5 MiB; формат определяется magic bytes (не Content-Type);
- png/jpeg/webp, влезающие в 1568 px по длинной стороне, — passthrough байт-в-байт;
- gif — всегда декодируется: первый кадр → PNG (анимация — не vision-вход);
- всё крупнее 1568 px — Catmull-Rom downscale до 1568 по длинной стороне;
- ре-энкод: PNG для png/gif-источников, если влезает в кап 1.5 MiB; иначе JPEG с деградацией качества 85→70→55→40 (шумные скриншоты падают в JPEG);
- итоговый блоб ≤ 1.5 MiB — запас под базовое расширение и лимиты пейлоада Temporal (блок пересекает границу activity → история workflow);
meta(mime/размеры/resized/байты/sha256-префикс) уходит вcontentтула и событиеtool.completed— блоб в журнал не попадает никогда.
#3.4. Ограничения v1
- требуется мультимодальная модель: текущий
glm-5.2(text-only) не потребляет image-блоки — нужен vision-тариф (у z.ai — линейка GLM-4V); - один инжект-user-сообщение на ход со всеми картинками хода (несколько
view_image/fetch_imageв одном ходе складываются в один блок-сообщение); - только png/jpeg/webp/gif-первый-кадр; без PDF/аудио/видео.
#4. Фаза 3 — Headless-браузер (реализовано)
Headless-браузинг для JS-рендеринга, скриншотов и интерактива. Контракт — Playwright-образное подмножество поверх CDP, поэтому бэкенды взаимозаменяемы: Lightpanda (по умолчанию, легковесный), Playwright-сервер или Chrome — без изменения тула.
#4.1. Тул browser
Один тул с параметром action:
| Action | Аргументы | Что делает |
|---|---|---|
open |
url |
Навигация таба рана + ожидание readyState=complete (бюджет 15s), возвращает финальный URL и title |
click |
selector |
scrollIntoView → координаты центра элемента → синтетический mousePressed+mouseReleased |
fill |
selector, text |
focus + очистка → Input.insertText (генерирует настоящие input-события, работает с SPA) |
evaluate |
expression |
Runtime.evaluate c returnByValue+awaitPromise; исключение — не ошибка тула, а результат exception: … |
content |
— | outerHTML отрендеренного DOM → readability → markdown (тот же пайплайн, что у web_fetch) |
screenshot |
— | Page.captureScreenshot (PNG) → пайплайн фазы 2 → vision-блок в следующем user-сообщении |
close |
— | Закрывает таб рана |
Одна вкладка на ран живет между вызовами (cookies/JS-состояние сохраняются до close) — подтверждено E2E живым JS-логином.
#4.2. Бюджеты и наблюдаемость
- глобальный лимит действий на процесс ядра (
KERNEL_BROWSER_PER_MIN, default 60) — скользящее окно как у web_search; - кап вкладок на процесс — 12 (lightpanda сам ограничивает 16 браузерных соединений);
- idle-сweep: вкладка неактивна 10 минут — закрывается при следующем любом вызове (забытые
closeпосле упавших ранов не копятся); - таймаут действия 45s, ожидание готовности страницы 15s;
- каждое действие — обычные
tool.started/tool.completedсобытия; экшены —browser.action, risk medium (клики меняют внешний мир), авто-апрув по политике песочницы как у web-тулов: изолированный браузер без креденшелов достигает лишь того, до чего песочница и так может дойти curl-ом.
#4.3. Сеть и изоляция
- lightpanda живет в отдельной docker-сети
browserи НЕ видит journal/postgres/temporal/kernel — это сетевой ответ на SSRF для браузера, который сам ходит за URL-ами; ядру назначены обе сети; --advertise-host lightpanda— Host-проверка CDP-websocket (анти-DNS-rebinding в lightpanda) принимает docker-dns имя;- discovery через
/json/version, но dial идёт на сконфигурированный базовый URL: анонсируемый URL содержит внутренний адрес бэкенда; - SSRF-guard web_fetch сюда не применяется (эндпоинт оператора, как SearXNG); браузер без креденшелов, метаданные облачных провайдеров — задокументированный остаточный риск (как и у любого egress-контейнера).
#4.4. Конфигурация
| Переменная | Значение | Default |
|---|---|---|
KERNEL_BROWSER_CDP_URL |
базовый http(s) URL CDP-бэкенда; пусто — тул не регистрируется | http://lightpanda:9222 (compose), пусто в bare-окружении |
KERNEL_BROWSER_PER_MIN |
глобальный бюджет действий/мин на ядро | 60 |
Регистрация: только при network_access рана + сконфигурированном бэкенде.
#4.5. Ограничения v1
- одна вкладка на ран (нет параллельных страниц одного рана);
- нет cookie/сессионной персистентности между ранами — каждый ран начинает со стерильного браузера;
- нет обработки JS-диалогов (alert/confirm), скачиваний файлов, установки viewport/UA (дефолты бэкенда);
- readLimit WS-сообщения 8 MiB — достаточно для скриншотов; PDF-принт не реализован.
#5. Наблюдаемость
tool.started/tool.completedсо стандартными полями (operation_id, arguments_hash) — ходы видны в Runs и чате как обычные тулы;- выдача web_fetch усечённо логируется (output в событии — как у остальных тулов);
- кэш выдачи не дублирует события: повторный fetch того же URL в течение TTL — один tool.completed.
#6. Критерии готовности фазы 1 (выполнены)
-
web_fetch: SSRF-guard + тесты (blocked private, redirect, капы, кэш, бинарное, JSON, HTML); -
web_search: SearXNG-стаб + форматирование, max_results, гейты; - гейты: без network_access тулы не регистрируются; без SearXNG-URL web_search не регистрируется;
- авто-апрув по политике песочницы;
- compose: сервис searxng + settings с json-форматом;
- E2E: живой ран — web_search → web_fetch → отчет (run
e2e-web2-*, проект lighthouse).