TESTED Microdesign v1.1 — aligned with code as of 4 May 2026. HTTP API Phase 1 (ADR 0078) implemented and verified live. 12/12 tests green via AioHTTPTestCase. Implementation: /opt/myclaw/runtime/metnos_http_server.py + http_auth.py + http_render.py + http_routes_agent.py + http_routes_admin.py + 8 Jinja2 templates.
Status in the sequence: under approvalapprovedtestedimplemented. Canonical page created on 4 May 2026 (ADR 0085). Sits alongside, not in place of, channel (Telegram remains the primary dialogue surface). See also pairing (multi-user via /start).
← Documentation index Microdesign › http_api

Metnos

http_api — admin server + uniform agent channel
Microdesign v1.1 — status TESTED (4 May 2026)
Audience: anyone who wants to talk to Metnos via HTTP/browser/curl.

Reading: 12 minutes.

Contents

  1. What it does: a second HTTP server
  2. Phase 1 endpoints
  3. Authentication and roles
  4. Content negotiation + ETag
  5. SSE on /agent/turn
  6. Dashboard /admin (htmx + uPlot)
  7. Relationship with channel (Telegram) and pairing
  8. Limits and Phase 2 deferrals

1. What it does: a second HTTP server

The HTTP API is the second Metnos server: listening on port 8770, separate from the remote-device pairing server (port 8765). It exposes three things: an admin dashboard in HTML for the host, a uniform agent channel (POST /agent/turn) for clients that prefer HTTP/SSE over Telegram, and node discovery (/.well-known/metnos.json) for the client side.

Picking a second server (instead of stretching the pairing server or funnelling everything through Telegram) is driven by three constraints: (a) the admin UI must live in a browser tab, so the host doesn't have to open Telegram to manage users, scheduler, introvertiva proposals; (b) the HTTP agent channel is the foundation for the Rust client and for future integrations; (c) the pairing server is minimal by design (port 8765, register-device only) and shouldn't grow.

Default bind: 127.0.0.1:8770. To expose to the LAN, change the bind in config (Phase 2 plans self-signed cert pinned by fingerprint, reusing pairing material).

2. Phase 1 endpoints

MethodPathRolePurpose
GET/agent/healthanonymousLiveness + uptime + version.
GET/.well-known/metnos.jsonanonymousDiscovery: name, channels, capabilities, admin-key fingerprint.
POST/agent/turnuser/adminRun run_turn. SSE if Accept: text/event-stream, JSON otherwise.
GET/agent/devices/meuserCaller device info (id, paired_at, autonomy).
POST/agent/registeranonymousRegister a device (pairing wrapper).
GET/adminadminDashboard root (HTML).
GET/admin/proposalsadminIntrovertiva proposals table (filter kind).
POST/admin/proposals/{sig_key}/{approve|reject|defer}adminAction on a single proposal.
GET/admin/executorsadminCatalog with lifecycle.
GET/admin/executors/statsadminCounts + daily events for uPlot.
GET/admin/runsadminLast N scheduler runs.
GET/admin/safetyadminSafety signatures (ADR 0071).
GET/admin/turnsadminLast N turns (jsonl).
GET/admin/usersadminUser table (host + guests).
POST/admin/usersadminCreate user from form (default owner = host).
GET/admin/users/{id}adminUser detail.
POST/admin/users/{id}/deleteadminDelete (cascade on user_channels).
POST/admin/users/{id}/autonomyadminChange autonomy_level.
POST/admin/users/{id}/channels/{channel}/pairadminIssue pairing token + instructions.
POST/admin/users/{id}/channels/{channel}/removeadminUnbind channel.

Collection routes (/admin/proposals, /admin/executors, /admin/runs, /admin/safety, /admin/turns, /admin/users) are negotiation-driven (see section 4).

3. Authentication and roles

Three roles, in increasing privilege order: anonymous < user < admin.

Admin key

File ~/.config/metnos/admin.key (mode 0600), 256-bit hex generated with secrets.token_hex(32) at first server boot. Logs only show the sha256 fingerprint (first 16 hex). The admin key also doubles as the master for credential encryption (see section 7 and ADR 0082).

Web login: POST /admin/login with the admin key in clear; response set-cookie with TTL 7 days (HttpOnly, SameSite=Strict). From CLI/curl: header Authorization: Bearer <admin.key>.

Device bearer token

The middleware compares the Bearer with the public_key_b64 of every paired device (table devices from pairing). Match → role user.

LAN trusted + anonymous whitelist

The «LAN trusted» rule is a consequence of the self-hosted model: on a home network the threat surface is small, and the host wants curl/htmx without juggling headers. The policy degrades the moment the bind opens to WAN (Phase 2 with TLS).

4. Content negotiation + ETag

Collection routes inspect Accept: text/html → Jinja2 fragment (htmx-friendly); JSON otherwise. In both cases an ETag is computed (sha256 of the payload, first 16 hex). If the caller resends If-None-Match with the same value, the response is 304 Not Modified with no body. Live smoke test verified 4 May 2026: ETag/304 round-trip OK.

5. SSE on /agent/turn

When the client sends Accept: text/event-stream, POST /agent/turn opens a stream and installs an _SSEProgress (implements runtime.progress.Progress) that run_turn calls at every step. Events emitted:

EventWhenPayload
thinkingStream open + each ongoing LLM call{"phase": "planner|intent|vaglio", "step": N}
progressUser notes (long-op notify){"text": "..."}
tool_callPhase 2 — not emitted today{"name", "args"}
tool_resultPhase 2 — not emitted today{"name", "ok", "preview"}
finalStream close with the final answer{"final_answer", "n_steps", ...}
errorError caught by the turn{"error_code", "message"}

run_turn runs in an executor thread (sync) while the main coroutine pumps events on the loop via asyncio.run_coroutine_threadsafe.

Phase 1 limit. tool_call and tool_result are not emitted: the current Progress interface does not yet have these callbacks. Phase 2 extends it.

6. Dashboard /admin (htmx + uPlot)

Frontend stack deliberately minimal: htmx + Jinja2 + uPlot via CDN, no build step, no CSS framework. Compact templates (<50 lines each) under runtime/templates/. Clean system-ui style, palette aligned with the rest of the canonical docs (navy / sage / bronze).

Main pages (Jinja2 rendering + htmx fragments):

7. Relationship with channel (Telegram) and pairing

Telegram remains the primary dialogue channel (long-poll, low cost, mobile UX consistency). The HTTP API is the alternative channel for:

  1. Host browser: /admin dashboard for administration (users, scheduler, introvertiva proposals, safety).
  2. Rust client (phase 7): remote turn from a paired Windows/Linux PC, via POST /agent/turn with SSE.
  3. curl/scripts: home automation shortcuts (curl -H "Authorization: Bearer ..." http://localhost:8770/agent/turn).

A run_turn invoked via HTTP receives channel="http"; no formal Channel is registered in channels.daemon (the poll-based model is incompatible with request/response). For vaglio and policy the effect is equivalent: steps go through the same gates, the audit keeps channel="http" in the turn log.

Multi-user (ADR 0083): the HTTP API is the admin point for the users.db table. Guest pairing via POST /admin/users/{id}/channels/telegram/pair: the admin gets a short-lived token, hands it to the guest, the guest sends /start <token> to the bot. The flow is described in pairing section 6.

Credentials (ADR 0082): the HTTP server admin key is also the master for symmetric encryption of ~/.config/metnos/credentials/<domain>.json.age. Whoever controls ~/.config/metnos/admin.key (mode 0600) can decrypt the credentials. Offline backup recommended.

8. Limits and Phase 2 deferrals

Live smoke verified 4 May 2026. GET /agent/health → 200 {ok:true, version:"1.1", uptime_s:...}. GET /.well-known/metnos.json → 200 with admin-key fingerprint. GET /admin (no auth, from loopback) → 403. GET /admin (Bearer admin key) → 200 HTML dashboard. If-None-Match round-trip → 304.

Code references: /opt/myclaw/runtime/metnos_http_server.py (entrypoint, factory, lock), http_auth.py (admin key + middleware), http_render.py (Accept negotiation + ETag), http_routes_agent.py (SSE, well-known, devices/me), http_routes_admin.py (read-only collections + actions), runtime/templates/ (8+ Jinja2 files). Tests: runtime/tests/test_http_server.py (12 tests). Reference ADRs: 0078 (HTTP API Phase 1), 0082 (credentials), 0083 (multi-user), 0085 (docs alignment 4/5/2026).