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:

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):

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):

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


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.