ADR 014 — Ready for others: measured capacity, sign-in for the registration change, a clear sign-in page
Date: 18 October 2026
On this page
Status: Accepted on 20 October 2026 after the Phase 13 acceptance (record). The owner took the five decisions below on 18 October 2026, and ended the sign-in watch after 39 hours on 20 October 2026.
Owner: Grok Gadgets hub (adidshaft/grok-gadgets)
Phase: 13, "Ready for others" (HUB-READY-001 #363). ADR numbers differ from phase numbers. The shared-relay decision is Phase 14 (#270).
Issues: gateway GW-SCALE-001 #181, GW-PROTO-002 #182, GW-COMPAT-002 #176, GW-UX-001 #207, GW-OAUTH-REFRESH-001 #211 (closed; residual below); hub #185, #269
Context
Phases 9 to 12a give a builder a relay, phone setup, claim codes, signed updates and a one-afternoon guide. The website already invites an outside builder. Strangers can arrive before four things are true:
- Nobody has measured a relay. No capacity statement exists.
- Sign-in uses a registration method the protocol deprecated. MCP 2026-07-28 prefers Client ID Metadata Documents (an HTTPS URL as
client_id). It keeps dynamic registration for at least twelve months (client registration). The Grok Bot connector backend registered by dynamic registration on 13 October 2026. - The sign-in page is plain text. In the Phase 11 test a person saw one bare field (#207).
- Grok Bot on Android has not called a gadget. Phone setup passed on Android; a Grok Bot call from Android did not happen.
Facts this design depends on (gateway 0.1.0a17, ESP32 v0.2.0-alpha.18, Linux 0.1.0a9):
- A relay holds at most 64 gadgets today. The relay listener accepts 64 sessions plus 32 waiting handshakes (
RELAY_SESSIONS,RELAY_PENDINGinservice.py). The device registry refuses a 65th device ID withbusy(domain.py). Protocol 0.1.0 states the 64-device limit. - One source address gets 20 connections a minute and 4 waiting handshakes. Each session may send 30 frames a second.
- Gadgets poll all the time. ESP32 firmware polls every 100 ms and sends state every 3 s (
GrokSession.h), so about 10.3 frames a second. A Linux agent on a relay polls every 500 ms. A session closes after 15 s without a request. - Idle memory is small; polling is the load. Research estimated tens of KB for each idle TLS connection (asyncio buffers; OpenSSL releases idle buffers). At 100 ms, 1,000 ESP32 gadgets send about 10,000 frames a second to one Python process. That is an estimate, not a measurement.
- The pinned MCP SDK (
mcp==2.3.0) looks up clients through one provider hook,get_client. It has no server-side code for metadata documents. The gateway's metadata saysclient_id_metadata_document_supported: false, so a client that follows the specification falls back to registration. - Grok Bot's access token lasts 1 hour (
ACCESS_SECONDS). Gateway0.1.0a17logs refresh results. On 17 October one automatic refresh was observed. Two earlier401 invalid_tokenfailures on0.1.0a16stay unexplained. - Grok Bot runs on iPhone (iOS 18+) and Android 9+ with the same Bots and connectors (mobile).
Decision
1. Capacity: measure, then state only what was measured (gateway #181)
A fleet tool. grok-gadgets-gateway fleet runs N simulated gadgets against a relay's device port. Each gadget:
- It connects with TLS, pins the relay CA and checks the name, as the ESP32 does.
- It sends a protocol 0.1.0 hello with its enrolled token.
- It polls at the firmware rate (100 ms by default;
--poll-ms) and sends state every 3 s. - It acknowledges each command at once with
executed.
The tool ramps up within the relay's per-address limits (18 new connections a minute by default). It does not bypass them, so the test exercises the real relay. A ramp to 1,000 gadgets takes about an hour.
A driver sends gadgets_command calls through the relay's MCP endpoint at a fixed rate (1 a second by default) to random gadgets. It times each call from request to the executed result.
grok-gadgets-gateway fleet sample --pid PID runs on the relay host. It writes the gateway's CPU share and resident memory every 5 s.
A relay option. serve --max-devices N (default 64, at most 2,000) sets the session limit and the registry limit together. Waiting handshakes stay at 32. The test needs it for 200 and 1,000 gadgets. A builder needs it to go past 64.
The run. The host has 512 MB of memory and one virtual CPU. We run 50, 200 and 1,000 gadgets, each for 15 minutes after the ramp:
| Measure | From |
|---|---|
| Gateway CPU share (mean, p95) | fleet sample |
| Gateway resident memory (peak) | fleet sample |
| Command latency (p50, p95, p99) | the driver |
| Frames a second offered and answered | the fleet tool |
Sessions refused, dropped or busy |
the fleet tool and the relay log counts |
The record keeps the raw CSV files and the command lines. It holds no token, address or host name.
What the docs say. The support matrix states the numbers measured on that host, with the host size, gateway version and poll rate. It makes no claim beyond the measured sizes. "One small host" is a measurement, not a guarantee. If a size fails, the docs say so and give the largest size that passed.
Long-poll follows the numbers (gateway #182). Take 1,000 gadgets polling every 100 ms. If p95 command latency stays under 2 s and CPU under 70 %, the size passes. Then long-poll goes to Later. Otherwise, or if the owner decides now, it ships as follows:
- A poll may carry
wait_ms(100–10,000). The device requests it; the relay never asks for it. - The relay holds that poll until a command, an update offer or a claim change is ready, or until
wait_mspasses. Then it answers as it does today. - A relay before this change answers the field with
invalid_request. The device then polls without it until the next boot, as withclaimandupdate. - A device that sends no
wait_msgets the same replies as today. Protocol 0.1.0 devices change in nothing. - The ESP32 SDK and Linux SDK opt in with a build or constructor setting. The kit needs one signed update to use it.
2. Sign-in by Client ID Metadata Document (gateway #176)
When client_id is an HTTPS URL, get_client fetches and checks the document before the sign-in starts:
- HTTPS only, port 443, a public DNS name; no IP-literal hosts, no user info, no fragment.
- The resolved addresses must all be public: no private, loopback, link-local, shared or reserved ranges. The connection goes to the checked address.
- At most 5 s and 16 KB; no redirects;
application/jsononly. - The document's
client_idmust equal the URL exactly.redirect_urismust be HTTPS, orhttpon loopback, as for registration.token_endpoint_auth_methodmust benone. - A cache keeps a good document for 10 minutes, at most 32 documents. A changed document replaces the cached one at the next fetch.
- The sign-in page shows
client_nameas self-declared ("calls itself …"). - Floods: fetches share the sign-in budgets of hub ADR 008. The relay logs a failed fetch once a minute (
oauth_cimd_rejectedwith reason; never the document).
Dynamic registration stays on. The metadata says client_id_metadata_document_supported: true only after the tests pass. Accounts mode stays the same. A metadata-document client has one client_id for every person. So the account still comes from the claim code and the refresh-token family, never from client_id (ADR 011).
Proof: a scripted client signs in by metadata document against a test relay, and registration still works. Grok Bot via metadata document is Not tested unless the relay log shows Grok Bot doing it.
3. A clear sign-in page (gateway #207)
- Project mark and colours, inline; phone-first, large type; light and dark.
- One code field shown as
XXXX-XXXX, upper case, paste-friendly; a drawing of a gadget screen showingCode XXXX-XXXX. - Plain errors: "That code did not work. Codes change every 10 minutes." and the attempts left.
- The page shows the name the client gives as self-declared.
- Kept: the redirect straight back to Grok Bot after a right code (GW-CLAIM-005), and a page policy without
form-action(GW-CLAIM-006). - Kept:
default-src 'none', no scripts, no external assets,X-Frame-Options: DENY,no-storeandno-referrer.
Proof: tests for content and headers; screenshots at phone size reviewed by the owner; then a stranger-style sign-in on the live relay.
4. Sign-in lifetime (#211 residual)
The access token keeps its 1-hour lifetime. The owner uses Grok Bot on three ordinary days with no fresh sign-in. The relay log must show oauth_token_refreshed lines and no 401 after an oauth_access_expired. If the watch shows a refresh failure, a new issue proposes a fix under the ADR 008 threat model. The lifetime does not change without that issue.
5. Platform statements
- Android: Grok Bot on Android calls a gadget through the relay (claim, then text on the kit). Until the owner sees that, the support matrix says "Grok Bot on Android: Not tested".
- Windows: the gateway, the SDKs and a relay are not tested on Windows. A relay runs on Linux. The docs say "Windows: Not tested" and do not claim it.
6. Calls for testers and builders
Drafts in community/drafts/phase13/ stay unposted: a tester call (#185), a builder call (#269) and one report template. The template asks for versions, steps, Pass / Fail / Not tested, what the tester saw and what they redacted. The owner posts them. Reports go to the issue; security reports go by private email.
7. Proof
Record docs/verification/ready-for-others-2026-10-DD.md. Each item says Pass, Fail or Not tested, the tier (software, owner, outside), and the log lines.
| Item | Tier |
|---|---|
| Load test at 50, 200, 1,000 on the chosen host | Owner-run host, software gadgets |
| Metadata-document sign-in by a scripted client; registration still works | Software |
| Grok Bot via metadata document | Not tested unless the log shows it |
| Three days of Grok Bot with no fresh sign-in | Owner |
| Sign-in page screenshots, then a stranger-style sign-in on the live relay | Owner |
| Grok Bot on Android: claim and text on the kit | Owner, if a phone is available |
| Kit on a released image by signed update, if firmware changed | Owner |
Results so far (18 October 2026)
Measured on a fresh 512 MB, 1-vCPU host (record):
- 50 and 200 gadgets polling every 100 ms passed. At 200 the relay used about 61 % CPU and 88 MB.
- 1,000 gadgets polling every 100 ms failed. CPU was saturated from about 400 gadgets, and the kernel killed the relay for memory at about 660.
- So, by the rule in §1, long-poll ships (gateway #182, ESP32 and Linux opt-ins).
- The memory came from asyncio's 256 KiB TLS read buffer; the relay now uses 32 KiB (gateway #235).
- With long-poll at 2,500 ms and the smaller buffer, 1,000 gadgets passed: about 67 % CPU and 137 MB, p95 command latency 406 ms.
Threat model
| # | Threat | Control |
|---|---|---|
| R1 | A metadata URL makes the relay fetch internal addresses (SSRF) | HTTPS on 443, public DNS names only, every resolved address checked, connect to the checked address, no redirects, size and time caps |
| R2 | A metadata document claims another client's name | client_name shown as self-declared; the claim code or the operator's approval decides every sign-in |
| R3 | Fetch floods | Shared sign-in budgets, a 32-entry cache, one log line a minute |
| R4 | --max-devices raised past what the host carries |
Docs give the measured sizes; the relay logs device_flood at the limit |
| R5 | A load test leaks data | Only test tokens on a fresh host; the record holds counts, never tokens, addresses or names |
| R6 | The new page breaks the phone sign-in sheet | Redirect and page-policy tests stay; a phone-size check before release |
| R7 | Long-poll holds sockets the relay needs | wait_ms capped below the 15 s idle timeout; held polls count in the session limit |
Owner decisions
Taken on 18 October 2026. The load test runs on a fresh droplet, destroyed after. Long-poll follows the numbers. The owner borrows an Android phone; the docs state Windows as Not tested. The calls go to GitHub issues #185 and #269 first. On the same day the owner's account held only the live relay droplet, so no earlier test host remained to delete.
- Load-test host: a fresh 512 MB droplet for a few hours, destroyed after (well under US$1 at hourly billing; the fleet runs from the Mac). Or the Mac only, which gives no relay-host numbers. Recommendation: the droplet.
- Long-poll: now, or Later unless the numbers ask for it. Recommendation: Later; decide from the numbers.
- Android phone: borrow one with Grok Bot for one session (the Phase 11 second account). Recommendation: borrow.
- Windows: state "Not tested", or find a Windows computer to test. Recommendation: state "Not tested".
- Where the calls go: GitHub issues #185 and #269 only, or also r/GrokGadgets and the owner's X account. Recommendation: the issues first; the owner decides on the others.
Not in scope
- The shared-relay decision (Phase 14, #270), connection research (#336), hardware identity (ESP32 #126), the Home Assistant bridge (HA #62) and gateway #221.
- Posting, secure boot, a kit binary, mains switching and words that encourage selling gadgets.