HTTP routes (http_route / static_route)
🔵 Agents and the global configuration can add their own
HTTP endpoints to the server without a rebuild — following the same model as script_tool /
script_hook. Two kinds of routes:
http_route— a handler in Rhai/JavaScript that responds to a request.static_route— serving static files (HTML/CSS/JS/assets) from a folder.
This complements, rather than replaces, the fixed surfaces: POST /v1/inbound/:template,
POST /v1/workflows/<name>/webhook and the telegram/email channels.
21.1 Layout
config/
├── routes/ # GLOBAL routes — picked up automatically
│ ├── weather.yaml + weather.rhai
│ └── assets.yaml + assets/ # static folder for static_route
└── agents/
└── felix/
├── agent.yaml # route_files: [routes/card.yaml]
└── routes/
├── card.yaml + card.rhai
└── static/ # static files added by the agent
Global routes are read from config/routes/*.yaml automatically.
Agent routes are wired in explicitly from agent.yaml:
# config/agents/felix/agent.yaml (felix — условное имя агента для примера)
route_files:
- routes/card.yaml
Agent routes are registered in the same router as global ones. By
convention (this is a warning, not an error) an agent route's path starts with
/ext/<agent>/.
21.2 http_route (scripted)
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>
Script contract
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) }
}
Return #{ status, headers, body } (all fields optional):
status— defaults to200.headers— a map; by defaultContent-Typeis chosen based on the type ofbody.body— a map → serialized to JSON (application/json); a string → served as is (text/plain).- A thrown exception →
500, the error is logged, the body is empty.
Helpers
The same engine and set of helpers as 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, as well as
db_query/db_schema/db_execute for reading/writing DuckDB (see §7.3) and
shell/exec/fs_read (see §7.3).
llm(...) is not available in routes (there is no agent process to charge the
budget to) — calling it will throw an error. There is no access to the core; state
is not preserved between calls (use HTTP or external storage/db_*).
21.3 static_route (files)
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
Behavior (GET/HEAD only):
*restis resolved to a path underdir; path traversal is blocked by checking that the resulting file stays insidedir.- Dotfiles and
.envare never served (deny-list). - Content-type is determined by extension (html/css/js/json/svg/png/jpg/…).
- No file →
404; a request for a folder →indexif set, otherwise404.
This is how an agent serves static files: static_route to the folder static/ inside its
routes/.
21.4 Auth
| Mode | Behavior |
|---|---|
public |
open (dev default) |
bearer |
requires Authorization: Bearer <token> matching auth.admin_tokens (the name is logged, not the value) |
token_env |
requires a bearer token equal to the value of the env var from token_env (unset/empty = reject everything, fail-closed) |
user |
requires a valid end-user identity: HMAC X-User-Token when auth.web_user_secret_env is set, otherwise trusts X-User-Id (dev). Exposes request.user_id / request.user |
auth: user is available from the first release.
21.5 Routing and lifecycle
- All routes are registered in a single router. A conflict of
path+methodwith a built-in route (/,/health,/v1/*,/login,/proxy*,/widget*, …) or with another custom route is rejected at load time (the route is skipped, an error is written). - Built-in routes always win; custom ones must not shadow them.
- Match priority among custom routes: exact literal >
:param>*rest. An exacthttp_routeon/ext/git/api/dashboardalways beats a wildcardstatic_routeon/ext/git/*rest— you can keep a single namespace for static files and the API. With equal specificity, the one registered earlier wins. - Recommended namespaces: global →
/ext/..., agent →/ext/<agent>/.... - Live reload (
kernel.live_reload, on by default):config/routes/**and agentroute_filesare re-registered on change (add/remove/update), just like tools and hooks. - Handlers are synchronous → they run on a blocking pool (not the async runtime),
bounded by
timeout_msandmax_operations.
21.6 Access log
Every request to a custom route is written to logs/access.jsonl /
logs/access.db, just like built-in ones, but with the route name and its scope
(global / <agent>) instead of a free-form path. Bodies, headers, and token
values are not logged.
21.7 Testing (without a server)
agent-os route config/routes/weather.yaml '{"method":"GET","path":"/ext/weather/moscow"}'
agent-os route config/agents/felix/routes/card.yaml --method POST --body '{"user_id":"x"}'
agent-os route config/routes/dash.yaml --args-file req.json # request JSON из файла ('-' = stdin)
agent-os route prints a fake request, the script result (status + headers + body),
and script errors — the analogue of agent-os hook / agent-os tool for routes. For
static_route it shows which files resolve under dir. Query parameters with
?/& are more conveniently passed via --args-file so the shell doesn't mangle them.