21. HTTP-маршруты (http_route / static_route)
🔵 Агенты и глобальная конфигурация могут добавлять на сервер собственные
HTTP-эндпоинты без пересборки — по той же модели, что script_tool /
script_hook. Два вида маршрутов:
http_route— обработчик на Rhai/JavaScript, отвечающий на запрос.static_route— отдача статики (HTML/CSS/JS/ассеты) из папки.
Это дополняет, а не заменяет фиксированные поверхности: POST /v1/inbound/:template,
POST /v1/workflows/<name>/webhook и каналы telegram/email.
21.1 Раскладка
config/
├── routes/ # ГЛОБАЛЬНЫЕ маршруты — подхватываются сами
│ ├── weather.yaml + weather.rhai
│ └── assets.yaml + assets/ # папка статики для static_route
└── agents/
└── felix/
├── agent.yaml # route_files: [routes/card.yaml]
└── routes/
├── card.yaml + card.rhai
└── static/ # статика, добавляемая агентом
Глобальные маршруты читаются из config/routes/*.yaml автоматически.
Агентские подключаются явно из agent.yaml:
# config/agents/felix/agent.yaml
route_files:
- routes/card.yaml
Агентские маршруты регистрируются в тот же роутер, что и глобальные. По
соглашению (это warning, не ошибка) путь агентского маршрута начинается с
/ext/<agent>/.
21.2 http_route (скриптовый)
kind: http_route
name: weather # id маршрута (по умолчанию — имя файла); пишется в access-лог
method: GET # GET|POST|PUT|PATCH|DELETE|HEAD|* (по умолчанию GET)
path: /ext/weather/:city # сегменты :param и *rest; путь должен быть уникальным
script: weather.rhai # .rhai или .js, относительно этого YAML
auth: public # public | bearer | token_env | user
token_env: "" # env с bearer-токеном (при auth: token_env)
max_operations: 10000000 # бюджет инструкций скрипта
timeout_ms: 30000 # потолок времени на обработчик
max_body_bytes: 1048576 # лимит тела запроса (по умолчанию 1 МиБ)
# + любые свои поля → доступны в скрипте как request.config.<field>
Контракт скрипта
fn handler(request) {
// request = #{
// method: "GET",
// path: "/ext/weather/moscow",
// params: #{ city: "moscow" }, // захваты :param / *rest
// query: #{ unit: "c" },
// headers: #{ "content-type": "application/json" },
// body: "...", // сырое тело строкой ("" если нет)
// json: #{ ... }, // распарсенное JSON-тело, или ()
// user_id: "...", // разрешённая идентичность, или ()
// user: "...", // канонический id пользователя, или ()
// config: #{ ... } // сырой YAML, включая свои поля
// }
if request.user_id == () {
return #{ status: 401, body: #{ error: "unauthorized" } };
}
let r = http_get(`https://api.weather.example/${request.params.city}`);
if r.status != 200 {
return #{ status: 502, body: #{ error: r.error } };
}
#{ status: 200, body: parse_json(r.body) }
}
Возврат #{ status, headers, body } (все поля опциональны):
status— по умолчанию200.headers— карта; по умолчаниюContent-Typeвыбирается по типуbody.body— карта → сериализуется в JSON (application/json); строка → отдаётся как есть (text/plain).- Выброшенное исключение →
500, ошибка логируется, тело пустое.
Помощники
Тот же движок и набор помощников, что у script_tool: http_get/post/put/patch/delete,
http(method, …), env_var, now, sleep_ms, urlencode/urldecode,
parse_json/to_json, json_get, extract_script_json, print/debug, а также
db_query/db_schema/db_execute для чтения/записи DuckDB (см. §7.3) и
shell/exec/fs_read (см. §7.3).
llm(...) в маршрутах недоступен (нет процесса агента, на который списать
бюджет) — вызов бросит ошибку. Нет доступа к ядру; состояние между вызовами не
сохраняется (используй HTTP или внешнее хранилище/db_*).
21.3 static_route (файлы)
kind: static_route
name: felix_assets
path: /ext/felix/*rest # должно заканчиваться на *rest
dir: static/ # папка относительно этого YAML
index: index.html # отдаётся при обращении к "/" (опционально)
auth: public
cache_ttl_secs: 300 # Cache-Control (опционально)
mime: # переопределение content-type по расширению (опционально)
.wasm: application/wasm
Поведение (только GET/HEAD):
*restрезолвится в путь подdir; path traversal блокируется проверкой, что итоговый файл остаётся внутриdir.- Dotfiles и
.envне отдаются никогда (deny-list). - Content-type определяется по расширению (html/css/js/json/svg/png/jpg/…).
- Нет файла →
404; запрос на папку →index, если задан, иначе404.
Так агент отдаёт статику: static_route на папку static/ внутри его
routes/.
21.4 Auth
| Режим | Поведение |
|---|---|
public |
открыто (dev по умолчанию) |
bearer |
требует Authorization: Bearer <token>, совпадающий с auth.admin_tokens (в лог пишется имя, не значение) |
token_env |
требует bearer-токен, равный значению env из token_env (не задан/пуст = отклонять всё, fail-closed) |
user |
требует валидную идентичность конечного пользователя: HMAC X-User-Token, когда задан auth.web_user_secret_env, иначе доверяет X-User-Id (dev). Отдаёт request.user_id / request.user |
auth: user доступно с первого релиза.
21.5 Роутинг и жизненный цикл
- Все маршруты регистрируются в один роутер. Конфликт
path+methodсо встроенным маршрутом (/,/health,/v1/*,/login,/proxy*,/widget*, …) или с другим кастомным маршрутом отклоняется при загрузке (маршрут пропускается, пишется ошибка). - Встроенные маршруты всегда выигрывают; кастомные не должны их затенять.
- Приоритет совпадения среди кастомных: точный литерал >
:param>*rest. Точныйhttp_routeна/ext/git/api/dashboardвсегда выигрывает у wildcardstatic_routeна/ext/git/*rest— можно держать единый неймспейс для статики и API. При равной специфичности выигрывает зарегистрированный раньше. - Рекомендуемые неймспейсы: глобальные →
/ext/..., агентские →/ext/<agent>/.... - Live reload (
kernel.live_reload, по умолчанию вкл.):config/routes/**и агентскиеroute_filesперерегистрируются при изменении (add/remove/update), как инструменты и хуки. - Обработчики синхронны → выполняются на blocking-пуле (не async-runtime),
ограничены
timeout_msиmax_operations.
21.6 Access-лог
Каждый запрос к кастомному маршруту пишется в logs/access.jsonl /
logs/access.db, как и встроенные, но с именем маршрута и его скоупом
(global / <agent>) вместо свободного пути. Тела, заголовки и значения
токенов не логируются.
21.7 Тестирование (без сервера)
route_tool config/routes/weather.yaml '{"method":"GET","path":"/ext/weather/moscow"}'
route_tool config/agents/felix/routes/card.yaml --method POST --body '{"user_id":"x"}'
route_tool config/routes/dash.yaml --args-file req.json # request JSON из файла ('-' = stdin)
route_tool печатает фейковый запрос, результат скрипта (status + headers + body)
и ошибки скрипта — аналог hook_tool / site_tool для маршрутов. Для
static_route показывает, какие файлы резолвятся под dir. Query-параметры с
?/& удобнее передавать через --args-file, чтобы их не портил шелл.