Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

rypsok reads one TOML file. It is located by --config <path>, then by the environment variable RYPSOK_CONFIG, then not at all: with no file, every setting takes the default below and the keyless engines are used. There is no other source of settings and no hot reload; a change takes effect at the next start.

rypsok check validates a file and prints the effective settings on standard error. Every problem names its key. Unknown keys and tables are refused. See What rypsok check says below.

Every default is a value of the limits table; the LIM- identifier in the right column names the row.

Top level

KeyDefaultRangeMeaning
config_versionrequired1Schema version; this release reads 1

[service]

The transport is not a setting: rypsok serve --transport stdio|http chooses it on the command line, where the launching host can see it.

KeyDefaultRangeMeaning
structured_contenttruebooleanReturn results as structuredContent plus text, or as text alone
user_agent_contact""a URL or an addressAppended to the User-Agent so that providers can reach the operator
shutdown_ms3000500–10000Time to exit after the end of input or SIGTERM (LIM-O1): the HTTP listener waits this long for its connections to finish, then closes them

[service.http]

KeyDefaultRangeMeaning
bind"127.0.0.1:8737"address and portListener of serve --transport http; off loopback it needs callers
allowed_hosts[]host names, host:portHost values accepted; empty means loopback names and the bind host; required for a bind on 0.0.0.0 or ::
allowed_origins[]origins with a scheme, :* for any port, nullOrigin values accepted; empty means every present Origin is refused
legacy_revisionstruebooleanServe the handshake-based revisions with sessions in memory; false refuses them (initialize gets error -32022); the 2026-07-28 revision is always stateless
max_connections641–4096Connections accepted at once (LIM-H1)
max_request_bytes655364096–1048576Size of a request body (LIM-H2); the same 413 whether declared or streamed
idle_timeout_s605–600Idle connection timeout (LIM-H3): HTTP/1 closes after it, a connection that has sent nothing, or has not yet shown its protocol, included; HTTP/2 pings after it and closes when the ping goes unanswered
max_streams161–256Streams one HTTP/2 connection may open at once (LIM-H4)
max_sessions2561–2048At most this many live sessions of the handshake-based revisions (LIM-H6); a handshake beyond it is answered 503 with Retry-After: 30, a hint, and opens no session. The 2026-07-28 revision keeps no session and is not counted. Sessions expire after 300 s without traffic. Each costs about 60 KiB, so 2048 sessions are about 120 MiB
request_timeout_s303–600, at least fetch.deadline_ms + 2 s (depends on fetch.deadline_ms)A request still running after this, body and handler included, is answered 408 (LIM-H5); a slow client cannot hold a connection

rypsok does not terminate TLS: off loopback, put a reverse proxy or a tunnel in front and list its host in allowed_hosts. Browsers never call rypsok, so allowed_origins stays empty unless a browser-based host needs it.

[service.http.auth]

KeyDefaultRangeMeaning
callers[]array of tablesThe callers and their tokens; required off loopback, optional on loopback (every request is then the caller local)

Each [[service.http.auth.callers]] table has id (lowercase letters, digits and underscores, starting with a letter, unique) and exactly one of token_env (the variable holding the token) or token_file (the file whose trimmed content is the token). A token is at least 32 bytes and is never written inline. A caller presents it as Authorization: Bearer <token>; rate limits apply per caller.

[[service.http.auth.callers]]
id = "mimlys"
token_env = "RYPSOK_TOKEN_MIMLYS"
KeyDefaultRangeMeaning
languages["en"]language tagsOffered to callers; the first is the default
region"US"two uppercase lettersPassed to engines that support it
safe_search"moderate"off, moderate, strictPassed to engines that support it
max_results51 to max_results_ceiling (depends on max_results_ceiling)Default of the max_results parameter (LIM-S2)
max_results_ceiling201–20Upper bound of max_results (LIM-S2)
query_max_chars4001–600Length of query (LIM-S1)
deadline_ms2500500–10000Overall deadline of a search (LIM-S4)
engine_timeout_ms2000100 to deadline_ms − reserve_ms (depends on deadline_ms and reserve_ms)Timeout of one engine request (LIM-S5); the message names both keys and their values
connect_timeout_ms1000200–5000Connect timeout of an engine request (LIM-S12)
reserve_ms20050–1000Reserve for post-processing (LIM-S6)
engines_per_request81–16Engines queried per request (LIM-S8)
results_per_engine105–20Results requested from each engine (LIM-S3)
snippet_max_chars30050–1000Length of a snippet (LIM-S9)
title_max_chars15050–300Length of a title (LIM-S10)
drop_flagged_resultsfalsebooleanDrop results with instruction-like content instead of only noticing them
engine_attributionfalsebooleanAlways carry results[].engines

[fetch]

KeyDefaultRangeMeaning
max_chars3000500 to max_chars_ceiling (depends on max_chars_ceiling)Default of max_chars (LIM-F1)
max_chars_ceiling20000500–100000Ceiling of max_chars (LIM-F2)
max_bytes524288065536–10485760Response bytes after decompression (LIM-F3). Memory grows with it: a page of this size costs about 5 times as much while it is parsed (rypsok check prints the worst case for your settings)
deadline_ms100001000–30000Overall deadline of a fetch (LIM-F4)
connect_timeout_ms3000500–10000Connect timeout (LIM-F6)
max_redirects50–10Redirects followed (LIM-F7); every hop counts, a cross-host one too when follow_cross_host_redirects is on
ports[80, 443]portsPorts a fetch may use (LIM-F8); another port needs an egress.allow rule
robots_txttruebooleanHonor robots.txt
follow_cross_host_redirectsfalsebooleanFollow a redirect to another host instead of returning redirected
strict_modefalsebooleanweb_fetch accepts only URLs that web_search returned in this process and URLs matched by an egress.allow rule; anything else is denied with reason strict_mode

[egress]

The agent-directed profile (fetches and robots.txt reads) refuses every destination that is not a globally routable unicast address: loopback, private, link-local, unique-local, multicast, carrier-grade NAT, documentation, benchmarking, NAT64 and cloud metadata ranges, every other block that the IANA special-purpose address registries list as not globally reachable, the deprecated site-local and IPv4-compatible forms, and names such as localhost, single labels, *.local, *.internal and *.arpa. The judgment is made on every address that is connected to, at connect time. Rules are applied in the order deny, allow, built-in: a deny rule refuses even what an allow rule admits, and an allow rule admits a private destination at the operator’s own responsibility. A rule is a network in CIDR notation (10.0.0.0/8, fd00::/8), an address, a host name (wiki.corp.example) or a subdomain pattern (*.corp.example, which does not match corp.example itself); never a scheme or a port. Names are compared lower-cased and without the root dot (blocked.example. is blocked.example), and a name with an empty label (a..example) is refused as malformed, never matched. Write a rule without the root dot and without a leading dot: a rule with one is a configuration error.

KeyDefaultRangeMeaning
allow_httpfalsebooleanAllow plain HTTP fetches; otherwise an http URL is retried as https
allow[]rulesAdmitted explicitly, also under fetch.strict_mode
deny[]rulesRefused in addition to the built-in refusals
proxy""URLProxy for engine traffic only; fetches never use a proxy, because rypsok cannot verify that it applies the same profile; environment proxy variables are ignored everywhere

[limits]

KeyDefaultRangeMeaning
outbound_in_flight321–256Outbound requests in flight, whole process (LIM-C1)
per_engine_in_flight21–8Requests in flight per engine (LIM-C2)
fetches_in_flight41–64Fetches in flight, whole process (LIM-C3)
parsing_in_flight21–16Parsing tasks in flight (LIM-C5)
web_search_per_minute601–6000web_search calls accepted (LIM-C6)
web_fetch_per_minute301–6000web_fetch calls accepted (LIM-C7)
engine_status_per_minute301–6000engine_status calls accepted (LIM-C8)
strict_mode_urls10000100–1000000URLs remembered for fetch.strict_mode (LIM-F14)
robots_cache_entries100010–100000robots.txt entries kept in memory (LIM-F15)

[resilience]

KeyDefaultRangeMeaning
failures_to_open31–10Consecutive failures that open an engine’s circuit (LIM-B1)
first_cooldown_s6010–600, at most max_cooldown_s (depends on max_cooldown_s)First cooldown (LIM-B2)
max_cooldown_s90010–3600Longest cooldown (LIM-B3); not below first_cooldown_s (the message is reported on first_cooldown_s)

[privacy]

KeyDefaultRangeMeaning
secret_detection"refuse"off, warn, mask, refuseWhat happens when a query or URL looks like it carries a credential (LIM-P1)

What the check recognizes: credentials with a fixed prefix or structure and the length of the real format, never a guess from randomness. AWS access key ids and secret access keys (the latter by name: aws_secret_access_key=…), GitHub, GitLab, npm, Hugging Face, Slack, Stripe, Google, OpenAI and Anthropic keys, SendGrid, Telegram bot and Twilio keys, JSON Web Tokens, Bearer tokens (also written Bearer%20…), Authorization: Basic headers, Azure AccountKey= values, PEM private keys, user and password in a URL (https://user:password@host/), and a value given to password=, passwd= or secret= (also client_secret=) when it has a digit or a symbol. A credential with an invisible character inside, or in lower case, or a GitHub token wrapped by a space, is found too. Ordinary text near these shapes is not: sk- followed by hyphenated words, bearer and a phrase, a 20-letter word after ASIA, host.example:8080@mirror and password=configuration all pass. The check names the kind and the position, never the value. One mode applies to all patterns; there is no per-pattern setting.

[logging]

KeyDefaultRangeMeaning
level"info"error, warn, info, debugLog level (LIM-P2)
log_queriesfalsebooleanDebug mode: query text in debug events; serve warns at start (LIM-P3)

Every line is one JSON object on standard error with timestamp, level, event and the fields of the event. Every event of a tool call carries two more fields, request_id and caller, without the event naming them: the id is a per-call identifier (the start time in milliseconds and a sequence number, in hexadecimal), unique within the process and different for every call; the caller is the id of [[service.http.auth.callers]] that made the call, or local over stdio and on a listener without callers (the operator’s label, never a token). They are on the events of the engines, the refusals, fetched and the closing call event alike, and at every log level. The id is not returned to the caller and is in no response. Events of the MCP SDK and of the HTTP stack (rmcp, hyper) are written outside the call and have neither field, and so does an event of the listener’s gate (a refused request) or a tool call that the adapter refuses before the service sees it (a rate limit): those have no id yet.

[status]

KeyDefaultRangeMeaning
enabledtruebooleanOffer engine_status; when false it is absent from the tool list

[cache]

The result cache: one entry per engine answer, per caller, in memory only. Off by default. Enabling it retains the queries of the last ttl_s seconds in memory as cache keys; nothing is written to disk. Only web_search answers of engines whose admission record allows storage (Wikipedia; not Brave) are cached, and a response built from a cached answer carries the notice from_cache.

KeyDefaultRangeMeaning
enabledfalsebooleanWhether answers are cached (LIM-K1)
ttl_s30010–3600Lifetime of an entry (LIM-K2)
max_entries10001–100000Entries held at most; the least recently used goes first (LIM-K3)
max_entry_bytes327681024–1048576An answer above this size is not stored (LIM-K4); memory is at most max_entries × max_entry_bytes

[scraping]

Scraping drivers read a provider’s human-facing page because no interface for programs exists. They are off by default; a table whose driver scrapes is refused at startup until this switch is on. Turning it on is your acceptance of the provider’s terms: rypsok sends its standard User-Agent and never imitates a browser, so a provider that refuses it answers with a challenge and the engine reports blocked. The endpoint’s robots.txt is recorded in the admission record, not consulted at run time.

KeyDefaultRangeMeaning
enabledfalsebooleanWhether scraping drivers may run (LIM-G1)

[engines.<id>]

One table per engine. The identifier is lowercase letters, digits and underscores, starting with a letter. With no [engines.*] table at all, two engines are configured: wikipedia (keyless) and brave (keyed by BRAVE_API_KEY).

KeyDefaultRangeMeaning
typerequiredwikipedia, brave, duckduckgoThe driver; duckduckgo scrapes and needs [scraping] enabled = true
enabledtruebooleanWhether the engine is used
categoriesrequiredgeneral, news, science, codeThe categories the engine serves; at least one
preference100integerLower first when results tie
api_key_env—variable nameWhere the credential is read from (keyed drivers)
api_key_file—pathAlternative to api_key_env; the file’s content, trimmed
endpoint"web"web, newsBrave only: which endpoint the engine queries
base_url—an absolute URLWikipedia only: a private MediaWiki mirror; plain HTTP is admitted on a loopback host only

A credential is never given inline: api_key = "…" is refused at startup. When the variable is unset or the file unreadable, the engine is inactive and the log says so; the process still starts.

Example

config_version = 1

[search]
languages = ["en", "ja"]
max_results = 8

[engines.wikipedia]
type = "wikipedia"
categories = ["general", "science"]
preference = 10

[engines.brave]
type = "brave"
categories = ["general"]
preference = 20
api_key_env = "BRAVE_API_KEY"

[engines.brave_news]
type = "brave"
categories = ["news"]
endpoint = "news"
api_key_env = "BRAVE_API_KEY"

What rypsok check says

rypsok check --config <file> exits 0 for a file serve would accept and 1 for one it would refuse. It checks everything serve checks, and one thing more than the ranges: when the file names callers ([[service.http.auth.callers]]), it reads their tokens as serve --transport http does. A token variable that is unset, a token file that cannot be read and a token shorter than 32 bytes are errors in check with the message serve gives. A file without callers and with a loopback bind reads no token.

It then prints, on standard error, one line for each part of the configuration, with the values that are in effect (a default is shown as a default): search, the tool rate limits, the call maxima, each engine and its credential’s state, the categories served, service.http (the bind, the number of callers, legacy_revisions and LIM-H1 to LIM-H6), fetch, egress (the number of allow and deny rules, allow_http, strict mode, whether a proxy is set), privacy, logging, cache, scraping, resilience, limits and the memory estimate (its session share only while legacy_revisions is on, since sessions exist only then). It never prints a token, an API key, the text of an egress rule or the address of a proxy, and no message repeats a value from the file: an unknown word for a setting is “an unknown word”, and a name in api_key_env or token_env is shown only when it looks like a variable name.

rypsok: configuration from --config rypsok.toml is valid
rypsok: service.http: bind=127.0.0.1:8737 callers=0 legacy_revisions=true max_connections=64 max_request_bytes=65536 idle_timeout_s=60 max_streams=16 request_timeout_s=30 max_sessions=256
rypsok: fetch: ports=80,443 max_bytes=5242880 max_chars=3000 (ceiling 20000) deadline_ms=10000 max_redirects=5 robots_txt=true follow_cross_host_redirects=false
rypsok: egress: allow_rules=0 deny_rules=0 allow_http=false strict_mode=false proxy=none
rypsok: privacy: secret_detection=refuse
rypsok: logging: level=info log_queries=false
rypsok: cache: enabled=false max_entries=1000 ttl_s=300 max_entry_bytes=32768
rypsok: scraping: enabled=false
rypsok: resilience: failures_to_open=3 first_cooldown_s=60 max_cooldown_s=900
rypsok: limits: outbound_in_flight=32 per_engine_in_flight=2 fetches_in_flight=4 parsing_in_flight=2 strict_mode_urls=10000 robots_cache_entries=1000
rypsok: memory: worst case about 122 MiB (base 13 + bodies 20 + parses 74 + sessions 15 + cache 0 MiB; an estimate for the musl build)

(The search, rate-limit, call-maxima, engine and category lines come first. The last line names the build its figure is for: an estimate for the glibc build on a glibc build, whose figure is higher, and measured on Linux; not measured for this platform elsewhere.) The format of the lines may change in a minor version; the exit code does not.

The maxima line, rypsok: tool call maxima in ms: web_search=2800 web_fetch=11000; engine_status reads memory only, gives the longest a call of each tool can last, for the configuration checked: a web_search call ends at LIM-S7 (the search deadline plus 300 ms) and a web_fetch call at LIM-F5 (the fetch deadline plus 1000 ms), measured from the start of the call, with the existing unavailable / deadline error. engine_status answers from memory and has no maximum. check’s text is for people and may change in a minor version; for a program, the stable source is its own configuration plus the two fixed margins, LIM-S7 and LIM-F5, in the limits table.

Warnings

A warning is one line rypsok: warning: <key>: <what happens>; <the safer setting>. It does not change the exit code: the setting is allowed, and check tells you what it costs. The shipped example configurations give none, except the two their README names.

KeyWhenThe safer setting
egress.allow[N]A rule admits a network wider than /16 (IPv4) or /32 (IPv6), 0.0.0.0/0 and ::/0 includedHost names, or a narrower network
privacy.secret_detectionIt is "off""refuse", the default
service.http.bindThe bind is not loopback: the listener speaks plain HTTP, so tokens and traffic cross the network unencryptedBind loopback behind a TLS-terminating proxy
logging.log_queriesIt is true: query text is written to the debug logfalse
fetch.robots_txtIt is falsetrue
egress.allow_httpIt is truefalse
fetch.follow_cross_host_redirectsIt is truefalse
engines.<id>An enabled engine scrapes a human-facing page (type duckduckgo)An engine with an API
cache.enabledThe cache is on and no enabled engine allows its results to be stored (Wikipedia does, Brave and DuckDuckGo do not)Turn the cache off, or enable an engine that allows storage
memoryThe worst case is above 256 MiB, the MemoryMax of the documented unitLower fetch.max_bytes, limits.fetches_in_flight, limits.parsing_in_flight or service.http.max_sessions
service.http.auth.callers[N]A token file is readable by everyonechmod 600

There is no warning for legacy_revisions: serving the handshake-based revisions in parallel is the default and bounded by max_sessions.