Expand description
A synchronous HTTP/1.1 server, just large enough to serve the portal.
Nothing here is a general-purpose web server and nothing here should become
one. It answers requests from one browser on one machine over the loopback
interface, which is why it is a few hundred lines of std::net rather than
a dependency: every HTTP crate that does chunked responses and a thread pool
brings a tree semlith would then be shipping to every user of a local search
tool, and the parts of HTTP this needs are the parts that fit on a page.
Three rules are enforced here, before any handler sees a request, because they are the product’s privacy claim and a claim enforced in one place is one that can be read in one place:
-
Loopback only.
Server::bindbinds127.0.0.1and there is no flag to change it. -
A per-run token, carried in a request header. Generated at start, printed once in the URL, handed to the page from that URL, and sent back as
Semlith-Tokenon every API request. A request without it gets 401 and an empty body — not a login page, not an error object, nothing that tells a prober what is here.It was a cookie until 0.14.0, and a cookie is the wrong container for it:
localhostis one site however many ports are listening on it, soSameSite=Strictnever separated this server from a page served by anything else on 127.0.0.1. A header the browser will not attach cross-origin does separate them. The cost is that a browser attaches no header to a stylesheet, a font, a favicon or an<img src>, so the page and its own static assets are served without a credential — they are the same bytes in every copy of the binary — and everything that answers about this machine is not. -
A write has to come from this origin. Every request that is not a GET or a HEAD must carry
Sec-Fetch-Site: same-origin, or — for a client that sends no fetch metadata at all, which is every client that is not a browser — anOriginmatching this server or noOrigin. It must also carry a JSON content type, because a body a browser can send with no preflight is a body this server will not read. Both are checked before the token, so a cross-origin page learns nothing from the difference between a right and a wrong guess. -
A persisted agent key, and only for
/mcp. From 0.13.0 the daemon also answers MCP over HTTP, and that endpoint needs a credential a client can keep in a configuration file — which the session token can never be, because it changes every run. The two have deliberately separate lifetimes and separate reach: the agent key opens/mcpand nothing else, so a key in a config file can never reach the rotate, adopt or upgrade routes, and rotating the session token does not disconnect an agent. -
A
Hostheader that is localhost. A page on another origin cannot read a cross-origin response, but it can send the request, and a DNS name that resolves to 127.0.0.1 would otherwise reach this server with the browser’s cookies attached. Onlylocalhostand127.0.0.1, with an optional port, are answered.
Every response also carries a Content-Security-Policy that allows only
'self', and no CORS header is ever emitted — there is no origin this
server wants to be readable from.
Structs§
Enums§
- Refusal
- Why a request was refused, for the one-line-per-class count the daemon logs.
Constants§
- DEFAULT_
PORT - The port
semlith startlistens on unless told otherwise. - MCP_
PATH - The one path the agent key opens.
- PORT_
ENV - Overrides
DEFAULT_PORT. - TOKEN_
HEADER - The header the session token travels in.
Type Aliases§
- Handler
- A handler: everything the daemon serves, behind one function.