# Using Crux as Your Memory & Retrieval Backend CoreCrux gives agents three distinct surfaces: - HTTP on `14800` for the shared REST API (`/v1/query/*`, `/v1/facts`, `/v1/sessions/*`, health/metrics). - gRPC on `4007` for the data plane. - MCP on `14801` for agent-facing tools such as private facts, agent-scoped sessions, and handoff. Use HTTP for shared application data. Use MCP when you need agent identity, private memory, or handoff between agents. ## Your First 3 Calls New to CoreCrux? Start here: ``` 1. get_bootstrap("patterns", token_budget=500) → Learn token reduction, scan-expand, fact memory, and session patterns. 2. store_fact(entity="test", key="hello", value="world") → Store your first fact. Confirm you get a fact_id back. 3. query_facts(query="hello", token_budget=500) → Retrieve it. You should see your fact with confidence=1.0. ``` (Pass `token_budget` on every retrieval call, including these — it is the convention this guide teaches throughout.) That's it — you're connected and working. Read on for authentication, tool selection, and advanced patterns. This guide covers the core memory loop. The wider tool surface — constraints (`declare_constraint` / `check_constraints`), passports (`issue_passport`), `.cruxpack` portability, substrate entities/edges, the coordination board, and per-session token accounting (`session_token_usage`) — is catalogued in [`mcp-system-prompt.md`](mcp-system-prompt.md). From a shell, `corecruxctl start` is the canonical zero-to-first-loop on-ramp. ## Platform Availability and Onboarding Crux Daemon is local-first. The local fact store and session store are available even when no upstream CoreCrux platform is online yet. When you are connecting Crux to another system, start with: ``` 1. sync_status() → Inspect whether this node is running local_only, manual_sync, sync_enabled, or degraded. 2. get_bootstrap(topic="docs", query="integration") → Pull the current onboarding playbooks from the seeded bootstrap docs. 3. update_status() → Check whether this checkout is current, behind, ahead, diverged, disabled, or unavailable before proposing maintenance work. ``` Use the returned mode like this: - `local_only`: keep working against the local HTTP + MCP surfaces and tell the human that remote sync is optional follow-up work. - `degraded`: keep working locally, surface the degraded reason, and ask the human to fix the remote sync config only if they actually need upstream sync. - `manual_sync` or `sync_enabled`: proceed with automated integration and verify end-to-end behavior. Use `update_status` like this: - `behind`: a newer tracked commit exists; pull the backup and upgrade playbooks before changing the node. - `ahead` or `diverged`: do not ask for a blind pull; switch to a human-reviewed upgrade flow. - `disabled`, `unavailable`, or `error`: tell the human how to configure the git update-check inputs, but keep serving traffic locally. The compact onboarding starter pack is compiled into `BootstrapSeeder` in `crates/crux-observe/src/bootstrap.rs` and seeded only when the local fact store has no existing `__bootstrap__::` facts, so agents can pull it at runtime without a hosted dependency. Set `CORECRUXD_SEED_BOOTSTRAP=0` to leave a cold store unseeded. ## First Connect Endpoint Selection When you are connecting to Crux Daemon for the first time, discover endpoints in this order: 1. Same host: HTTP `http://127.0.0.1:14800`, MCP `http://127.0.0.1:14801/mcp`, gRPC `127.0.0.1:4007`. 2. Tailnet host: use the Tailscale MagicDNS name or `100.x.y.z` tailnet IP with the same ports. 3. Remote HTTPS: use an operator-provided reverse proxy only when the daemon is intentionally exposed outside the local host or tailnet. Do not inline bearer tokens into URLs, MCP config files, or hook definitions. Resolve them from `CRUX_AGENT_TOKEN`, `CRUX_AGENT_TOKENS`, a token CSV such as `~/.config/cuecrux/crux-tokens/MCP_AGENT_TOKENS_CSV`, or the host's secret manager. Run these checks before assuming integration is complete: ``` 1. GET /readyz on the HTTP endpoint. 2. GET /v1/version on the HTTP endpoint. 3. MCP initialize, then tools/list. 4. call cuecrux_session to get the typed capability plan. 5. call sync_status(), update_status(), and get_bootstrap(topic="patterns", token_budget=500). 6. store_fact followed by query_facts(token_budget=500) for an end-to-end memory check. ``` If the agent host supports native streamable HTTP MCP, configure the MCP URL and bearer-token environment variable directly. If Codex CLI cannot complete the Crux HTTP MCP startup handshake, use the first-party stdio bridge under `integrations/codex-cli/crux-mcp-stdio.py`; it speaks Codex stdio JSON-RPC on stdin/stdout and forwards calls to the Crux HTTP MCP endpoint selected by `CRUX_MCP_URLS`, `CRUX_MCP_URL`, `~/.config/cuecrux/env`, or localhost. ## Authentication HTTP and MCP use different authentication models: - HTTP auth is controlled by `CORECRUXD_AUTH_MODE`. - MCP auth is controlled by `CRUX_AGENT_TOKEN` / `CRUX_AGENT_TOKENS`. ### HTTP Set `CORECRUXD_AUTH_MODE` on the server. If the selected mode requires bearer tokens, send `Authorization: Bearer ` on HTTP requests. ### MCP If the server has no MCP agent tokens configured, MCP requests may run as `anonymous`. If the server sets `CRUX_AGENT_TOKEN` or `CRUX_AGENT_TOKENS`, every `POST /mcp` request and authenticated SSE stream must include the matching bearer token. Streamable HTTP SSE sessions are capped globally and per agent or client IP via `CRUX_MCP_SSE_MAX_SESSIONS` and `CRUX_MCP_SSE_MAX_SESSIONS_PER_OWNER`. ```bash export CRUX_AGENT_TOKEN="your-token-here" curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_identity","arguments":{}}}' ``` ## Unified login (`crux login`) One command authenticates a client to a daemon wherever it lives, auto-selecting the lowest-friction *secure* rail. The shipped binary is `corecruxctl`: ```bash corecruxctl login # discover + auto-select a rail corecruxctl login --url https://crux.example.com corecruxctl login --token # CI / headless / air-gapped corecruxctl login --device # force the browser device-grant flow corecruxctl whoami # show stored credential posture per daemon corecruxctl logout --url # revoke + clear (or --all) ``` `login` discovers the daemon (`--url` → `~/.config/cuecrux/env` → localhost), probes `/readyz` + `/v1/version` for reachability and a Read route for the auth posture, picks a rail, persists the credential to `~/.config/cuecrux/credentials.json` (mode `0600`), registers the MCP endpoint in `~/.config/cuecrux/env`, and verifies `tools/list` + a `store_fact`→`query_facts` round-trip. The four rails, in auto-selection preference order: | Rail | When | Credential | |---|---|---| | 1 — loopback | same host, `auth=off` | none | | 2 — tailscale | verified tailnet identity (`tailscale serve`) | daemon-minted scoped JWT | | 3 — device | no host/env access (remote) | device-grant access + refresh token | | 4 — static token | `--token` / CI / air-gapped | operator-provided named token | An explicit `--token` or `--device` overrides auto-selection. Short-lived access tokens (≤5 min JWTs) auto-refresh; the device rail's refresh credential is named and revocable (`logout` revokes it). Off-host rails require encrypted transport (WireGuard for tailnet, TLS for remote) — a plaintext non-loopback `auth=off` bind is refused. **One credential for HTTP + MCP.** HTTP (`:14800`) and MCP (`:14801`) use different auth systems (signed JWT vs the `CRUX_AGENT_TOKENS` registry). To unlock both with a single login, set `CORECRUXD_HTTP_ACCEPT_AGENT_TOKENS=1`: under a JWT mode the HTTP API then *also* accepts a registered MCP agent token (mapped to `CORECRUXD_AGENT_TOKEN_HTTP_SCOPES` / `CORECRUXD_AGENT_TOKEN_HTTP_TENANT`). Then `corecruxctl login --token ` stores one long-lived token that works on both ports, survives daemon restarts, and works with native MCP clients that send a fixed bearer. An unmapped token acts as the namespaced automation principal `agent:`; only an explicit `CRUX_AGENT_PASSPORTS` entry maps it to a real passport. Default off, so HTTP stays JWT-only unless you opt in. Daemon-side rails 2 and 3 are opt-in and default off (see `config.example.env`: `CORECRUXD_TS_IDENTITY_ENABLED`, `CORECRUXD_DEVICE_GRANT_ENABLED`). Issuance mints HS256 JWTs, so the daemon must run in `jwt_hs256` mode. The issued `tenant_id` and scopes are always set by the approving identity (allowlist or device approver), never by the requesting client — this is what keeps cross-tenant issuance closed (threat ref T.1). Rail 2 works with **any** proxy that authenticates humans, not just Tailscale. `CORECRUXD_IDENTITY_HEADER` names the header your proxy sets; it defaults to the `tailscale serve` preset, so an existing deployment is unaffected. | proxy | `CORECRUXD_IDENTITY_HEADER` | |---|---| | `tailscale serve` (default) | `tailscale-user-login` | | oauth2-proxy | `x-forwarded-email` (or `x-auth-request-email`) | | Cloudflare Access | `cf-access-authenticated-user-email` | | Authelia | `remote-email` | | Authentik | `x-authentik-email` | > **The proxy must strip-then-set that header on every forwarded request.** A > trusted CIDR proves the proxy *sent* the request, not that it *authored* the > header — a proxy that forwards a client-supplied value hands any client a free > identity. `tailscale serve` and Cloudflare Access overwrite by construction; > oauth2-proxy and Authelia depend on how you configure them. If you cannot > guarantee it, use the device-authorization grant (rail 3) instead, which > derives identity from an approver rather than a header. Rail 2 also answers the **proxied console**. When the console sits behind an authenticating proxy that injects one shared admin JWT (no `passport_id`) and forwards each operator's verified login in the identity header, the human-decision routes — work-gate approve/reject and passport-mint approve/reject — consult the rail directly: a trusted proxy peer (loopback or `CORECRUXD_TS_TRUSTED_PROXY_CIDRS`) plus an allowlisted login whose entry carries `#` resolves the decision *as that passport*, and the receipt envelope records `approver_source` so the provenance is auditable. Without the `#` suffix the login is known but unbound and the decision is refused exactly as before; agent tokens, passport-header overrides and untrusted peers are refused unchanged. ## IX / Infra: machines, hooks, config & session sync Onboarding is login-driven and observable. `corecruxctl login` does three things: authenticates, installs the Claude Code hooks, and registers the machine. The console's **IX (Infra)** section surfaces it all. ```bash corecruxctl login # auth + hooks + machine capture (skip: --no-hooks / --no-register) corecruxctl machine list # machines logged into the daemon corecruxctl hooks install # (re)install Claude Code hooks; --user for ~/.claude, else project corecruxctl hooks status # what's wired # Carry a known-good Claude config across machines (secrets redacted): corecruxctl config push myles-pc # capture ~/.claude → daemon corecruxctl config pull myles-pc # deploy onto another machine (re-run `login` to re-fill secrets) corecruxctl config list # Share session-state snapshots across machines (event-driven, not real-time): corecruxctl session push --file state.json corecruxctl session pull corecruxctl session list ``` Storage model: machines, config bundles, and session snapshots are **public facts** under `__infra__::machines` / `__infra__::configs` / `__infra__::sessions` on the shared daemon, so every machine reads the same view. Config bundles carry *structure* (settings, `.mcp.json`, `CLAUDE.md`, `commands/` + `agents/`), not secrets — values under secret-looking keys and token-shaped strings are replaced with `${REDACTED}` and re-resolved per machine via `login`. They are readable by any `admin:read` caller on the daemon, so don't push data you wouldn't share within the daemon's tenant. Console: the IX section reads `GET /v1/console/infra/summary` (auth `admin:read`). On a remote `jwt_hs256` daemon, set a bearer once in the browser: `setConsoleToken("")` (stored in `localStorage`), then open the **IX** pill → Onboarding / Machines / Auth rails / Config bundles / Session sync. ## Human-Guided vs Automatic Integration Choose the integration style that matches the environment: - Human-guided: pull `get_bootstrap(topic="docs", query="Human-Assisted Integration")`, then tell the operator which endpoint, token, and verification call they need to run. - Automatic: pull `get_bootstrap(topic="docs", query="Automatic Integration")`, write the HTTP/MCP config into the host system, reload it, and verify with `/v1/version`, `tools/list`, `store_fact`, and `query_facts`. - Mixed mode: use HTTP for shared application state and MCP for private facts, agent-scoped sessions, and handoff. ## Upgrades and Backups Use the same central bootstrap docs for maintenance: - Human-guided upgrade: call `update_status()`, then pull `get_bootstrap(topic="docs", query="Human-Assisted Upgrade")`. - Automatic upgrade: only when `update_status().state == behind`, the repo is writable, and a backup path exists. Pull `get_bootstrap(topic="docs", query="Automatic Upgrade")`. - Backup options: pull `get_bootstrap(topic="docs", query="backup")` before changing the checkout or restarting the service. Current Crux Daemon backup rails are operator-level rather than one-click automation: - Filesystem or volume snapshots of `CORECRUXD_DATA_DIR` - `corecruxctl verify-store --data-dir ./data --scope recent` before and after upgrades - Replay or receipt exports from `/v1/replay/exports/*` - Git branch or tag markers before pulling when the service runs from a checkout ## Tool Decision Tree Choose the right query endpoint based on what you need: ``` Do you need full document content right now? ├── YES → POST /v1/query/text-search (standard query) │ Returns scored results with full content, coverage report. │ └── NO → Do you need metadata first to decide what to fetch? ├── YES → POST /v1/query/text-search with mode=scan │ Returns titles, scores, token counts — no content. │ Then: POST /v1/query/text-search/expand with result_ids │ to fetch full content for selected results only. │ └── Do you need related/connected documents? └── YES → POST /v1/query/graph-expand Traverses entity relationships with a hop budget. Use when you need "what else is related to X?" ``` **Summary:** | Endpoint | Use when | Returns | |---|---|---| | `text-search` | You know what to search for | Full results + coverage | | `text-search` (mode=scan) + `expand` | Large corpus, token-conscious | Metadata first, then selected content | | `graph-expand` | Exploring relationships | Connected documents via entity graph | | `time-range` | Temporal queries | Events within a time window | ## Fact Store Patterns The fact store is a receipted key-value memory scoped by entity. Each fact costs about 12 tokens versus replaying the original conversation. ### Shared facts over HTTP ```bash curl -X PUT http://localhost:14800/v1/facts \ -H "Content-Type: application/json" \ -d '{ "entity": "project-alpha", "key": "status", "value": "Phase 1 complete, 12 milestones delivered", "confidence": 0.95 }' ``` Response: ```json { "fact_id": "f_01J...", "receipt_id": "rcpt_01J...", "created_at": "2026-04-03T10:00:00Z" } ``` ### Query facts ```bash curl "http://localhost:14800/v1/facts?query=project+status&token_budget=500" ``` Returns facts matching the query, fitted within your token budget. HTTP `/v1/facts` is a shared surface. It does not support `private=true`. ### Citing Quality + Threat refs in design facts CueCrux uses a small, stable taxonomy so design decisions cite a fixed set of IDs instead of free-text rationale. The taxonomy is defined in [docs/quality-threat-refs.md](./quality-threat-refs.md) under "Quality refs" and "Threat refs". Refs themselves never change — `QC.1` means the same thing across every ExecPlan and every commit body — so they survive renaming, refactors, and time. When storing a design fact, embed the refs in the value JSON **and** write sibling tag facts so `query_facts` can retrieve "every design citing QC.3" cheaply: ```bash # Main decision fact. store_fact( entity="design:passport-routing", key="decision:tenant-isolation", value={ "rationale": "...", "qc_ref": ["QC.3"], "threat_ref": ["T.1"], "commit_sha": "abc123" } ) # Sibling tags — one per cited ref. Cheap (~12 tokens each) and queryable. store_fact(entity="design:passport-routing", key="qc_ref:QC.3", value={"cited_in": "decision:tenant-isolation"}) store_fact(entity="design:passport-routing", key="threat_ref:T.1", value={"cited_in": "decision:tenant-isolation"}) ``` Retrieval: ```bash # Every design fact citing QC.3, across all design entities. query_facts(entity_prefix="design:", key="qc_ref:QC.3", token_budget=2000) ``` This pattern uses the existing `query_facts` (no retrieval-layer change required) and stays compatible with `query_expand`'s `segment_index:doc_id` shape. The drift-check script `Crux/scripts/check-execplan-drift.sh` flags ExecPlans that cite a `decision:` whose sibling tags are absent — that's the integrity gate. ### Private facts over MCP Set `private: true` with the `store_fact` MCP tool to scope a fact to your authenticated agent only. Other agents cannot see it, and handoff packages do not export private facts. ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "store_fact", "arguments": { "entity": "my-agent", "key": "internal_state", "value": "Waiting for user confirmation on budget", "confidence": 1.0, "private": true } } }' ``` ## Session Persistence Patterns Sessions store structured state such as decisions, open questions, and constraints. ### Shared sessions over HTTP ### Save session state ```bash curl -X PUT http://localhost:14800/v1/sessions/session-42/state \ -H "Content-Type: application/json" \ -d '{ "decisions": ["Use PostgreSQL for primary store", "Deploy to eu-west-1"], "open_questions": ["Which caching layer?"], "constraints": ["Budget < $500/mo", "GDPR compliant"], "context": "Architecture review for Project Alpha" }' ``` ### Resume a session ```bash curl http://localhost:14800/v1/sessions/session-42/state ``` Returns the full session state, ready to inject into your context window. ### Agent-scoped sessions over MCP When you access sessions through MCP, authenticated agents read and write inside their own session namespace: ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "save_session", "arguments": { "session_id": "session-42", "state": { "decisions": ["Use PostgreSQL for primary store"], "open_questions": ["Which caching layer?"], "context": "Architecture review for Project Alpha" } } } }' ``` ## Multi-Agent Handoff Workflow (MCP Only) When Agent A finishes a task and needs to pass context to Agent B: ### Agent A: Create handoff package ```bash PACKAGE_JSON=$(curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_handoff", "arguments": { "session_id": "session-42", "include_facts": true, "target_agent": "agent-b", "message": "Completed architecture review. Three decisions made, one open question remaining." } } }' | jq -r '.result.content[0].text') ``` Pass `PACKAGE_JSON` to Agent B. The package is server-authenticated and includes only relevant non-private facts. ### Agent B: Accept handoff ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d "$(jq -nc --arg package "$PACKAGE_JSON" '{ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "accept_handoff", arguments: { package: $package } } }')" ``` Agent B now has full session state and facts from Agent A's work. If handoff packages must survive daemon restarts or move between replicas, set the same `CRUX_MCP_HANDOFF_SECRET` on every server instance. ## Coverage Awareness Every `text-search` response includes a `coverage` object: ```json { "coverage": { "score": 0.33, "gaps": ["quantum", "entanglement"] } } ``` **What to do when `score < 0.5`:** 1. **Broaden your query.** Try synonyms or more general terms. 2. **Check gaps.** Call `GET /v1/gaps` to see what the corpus is missing. 3. **Inform the user.** If the corpus genuinely does not cover the topic, say so rather than hallucinating an answer. 4. **Store a gap fact.** Record the coverage gap so future agents know: ```bash curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"coverage-gaps","key":"quantum-physics","value":"Corpus has no quantum physics content","confidence":1.0}' ``` ## Token Reduction Strategies ### Use `token_budget` instead of `top_k` ```bash curl -X POST http://localhost:14800/v1/query/text-search \ -H "Content-Type: application/json" \ -d '{"query": "deployment strategy", "token_budget": 4000}' ``` The engine returns the best results that fit within 4000 tokens. This reduces prompt cost by 60-80% compared to fixed `top_k`. ### Progressive retrieval (scan then expand) For large corpora, use the two-pass pattern: 1. **Scan** — get metadata only (titles, scores, token counts): ```bash curl -X POST http://localhost:14800/v1/query/text-search \ -d '{"query": "deployment", "mode": "scan", "top_k": 20}' ``` 2. **Expand** — fetch full content for the results you actually need: ```bash curl -X POST http://localhost:14800/v1/query/text-search/expand \ -d '{"result_ids": ["r_01", "r_05", "r_12"], "token_budget": 3000}' ``` This avoids pulling content you will discard. ### Pace self-paced loops under the prompt-cache TTL A `/loop` (or any self-paced wakeup) that sleeps an hour pays for the sleep. Claude Code caches the prompt prefix with a **1-hour TTL measured from request start**, so a 3600-second delay *plus* the time the previous turn took to answer lands past the TTL, and the next prompt re-writes the entire prefix at the 2x (1h) cache-write rate. Nothing about the loop looks wrong when this happens — the work still completes, it just costs double on that turn. **Cap a self-paced wakeup interval at ~50 minutes** (`/loop 45m`, not `/loop 60m`), or decide deliberately to eat the rewrite. Measured on corpus `drivew-host-claude-transcripts-2026-09` (the 27 most recent Claude Code sessions with >= 5 API turns on drivew-host, read 2026-09-17): idle TTL expiry caused **77.4%** of all rewritten cache tokens — 76 events, 29.0M tokens — and **7 of those 76 were 60-65 minute gaps**, i.e. a wakeup that missed the TTL by minutes and is free to recover by shortening the interval. To see the clock rather than guess at it, read `GET /v1/sessions/{sessionId}/cache`, which reports `ttl_seconds_remaining` and the `est_rewrite_tokens` a lapse would cost. The Crux statusline renders the same countdown (`cache 34m`, amber under 10 minutes, `cache COLD ~180k` once it has lapsed) at zero prompt tokens, because statusline text is not part of the prompt. ## Example Workflow: Research, Store, Save, Handoff A complete agent workflow showing all patterns together: ```bash # 1. Research: search the corpus curl -X POST http://localhost:14800/v1/query/text-search \ -H "Content-Type: application/json" \ -d '{"query": "database migration best practices", "token_budget": 4000}' # 2. Store facts extracted from results curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"db-migration","key":"rollback-strategy","value":"Always use reversible migrations with down scripts","confidence":0.9}' curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"db-migration","key":"zero-downtime","value":"Use expand-contract pattern for schema changes","confidence":0.85}' # 3. Save session state curl -X PUT http://localhost:14800/v1/sessions/research-42/state \ -d '{ "decisions": ["Use expand-contract pattern", "Require down migrations"], "open_questions": ["Which migration tool?"], "context": "Researching DB migration strategy for Project Alpha" }' # 4. Handoff to implementation agent via MCP curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "create_handoff", "arguments": { "session_id": "research-42", "include_facts": true, "target_agent": "implementer", "message": "Research complete. Two key decisions made. See facts for details." } } }' ``` ## Rate Limiting Crux has coarse built-in HTTP request caps and client-IP rate limiting. For production deployments, place Crux behind a reverse proxy (Caddy, nginx) with route-specific rate limiting configured. Set `CORECRUXD_TRUSTED_PROXY_CIDRS` only for proxy peers that strip inbound `Forwarded` / `X-Forwarded-For` and rewrite them from the real client address. The gRPC append path has built-in per-tenant throttling via `CRUX_TENANT_THROTTLE_*` env vars. Every HTTP endpoint has a **30-second request timeout** enforced by Tower middleware. Requests exceeding the deadline receive a `408 Request Timeout` response.