drogna

A demonstration harness. Every number in it is invented.

V1 record — describes retired software; superseded for V2 by srd.md, constitution 2.0.0 and ADR-0027.

ADR-0021: The clock's HTTP interface answers the browser from any origin

Status: Accepted (by the owner, 28 August 2026, by structured interview) — to be superseded by ADR-0025 when the clock's control surface is routed through the boundary (wave 7, lane I); it records the running system's shape until then Date: 28 August 2026 Requirements: SRD FR-10, FR-49, FR-53; ADR-0009; client FR-012 Raised by: long-run-01, commit e46bbf1, which found the fault and deliberately left the decision: "adding CORS to a control endpoint is a decision about what may reach it from where, and this branch has already spent one such decision provisionally"

This record is proposed, for the same reason ADR-0020 is: the change exists and ought not to sit in the tree undocumented, and the fact that it exists is not an argument that it is settled.

Context

The clock's HTTP interface exists for exactly two things a subscription cannot do (ADR-0009): setting the rate, and answering "what is the time now" for a component catching up. FR-10 and FR-49 say the rate control belongs in the browser, and FR-53 depends on it: a capture pins the clock to zero through the control the client offers a viewer.

The browser always arrives cross-origin. The page is served from the client's port (8080 locally) and the clock listens on its own (8090); a fetch carrying Content-Type: application/json between two origins is preflighted, and the clock's handler — built on the standard library's BaseHTTPRequestHandler — answered OPTIONS with 501 Unsupported method and no grant. The browser then never sends the POST and reports only "Failed to fetch". The rate control has therefore never worked from a browser, the one place it is for.

Nobody had seen it fail because nothing had ever exercised it. The capture workflow used to serve the built client with vite preview and no clock behind it, so pinIfAClockAnswers returned early on every run. The first time CI brought the real harness up (PR #21), the pin was reached for the first time and found the door shut.

Decision

The clock answers the preflight and states the grant on every response: Access-Control-Allow-Origin: *, with OPTIONS answered 204 naming the methods and the Content-Type header. No route, host or origin appears in the configuration for this, because the grant does not vary: the interface either serves browsers or it does not.

What this costs, stated plainly

CORS is a browser's rule about which pages may read which servers; it was never the clock's authentication, because the clock has none. Before this change, any local process could already command the clock with one curl; the bind address (127.0.0.1 in the local destination) was and remains the actual boundary.

What the wildcard adds is this: a page from any origin loaded in a viewer's browser — including a hostile site the viewer happens to visit — may now send commands to any clock its browser can reach, typically localhost:8090 on the viewer's own machine. Before, the failed preflight kept the command from being sent at all. The exposure is the ability to pause or accelerate a local demonstration clock, on a harness whose data is synthetic and says so; there is no credential to steal on this surface and no data to read that the query layer does not already publish. If that judgement is wrong, the revert is two lines in services/clock/src/harness_clock/http.py (the do_OPTIONS handler and the header in _write), and the pair capture returns to refusing on an unpinnable clock — loudly, as it did.

Alternatives rejected

Consequence for the acknowledgement, recorded with it

With the preflight answered, a second fault surfaced behind the first, and the two ship together because only together do they make the pin land. The client's rule (FR-012) is that the rate in force arrives only as a ctl/clock sample, and a request is resolved by a sample carrying a later tick. A rate of zero stops emission, so no later tick can ever acknowledge the one rate a capture needs. The clock now answers a command that stops emission by re-publishing the tick in force with the new rate and mode — one sample, under the same lock as emission — and the client accepts a same-tick sample carrying exactly the requested rate as the acknowledgement. The message schema's prose records the repeat; tick values remain quantised and no tick is invented, so ADR-0009's replay arithmetic is untouched.