summaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /AGENTS.md
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
v5.0.0: protocol v5 and the browser frontend
Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO.
Diffstat (limited to '')
-rw-r--r--AGENTS.md65
1 files changed, 62 insertions, 3 deletions
diff --git a/AGENTS.md b/AGENTS.md
index ab794f8..3827957 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,20 +37,79 @@
37 transaction. 37 transaction.
38 38
39## Crash 39## Crash
40- The client draws the multiplier from its own clock for display only. A 40- The client draws the multiplier from its own clock, corrected by the server
41 cashout carries a session id and nothing else; the multiplier and payout come 41 time in `HELLO`/`PONG`, for display only. A cashout carries a session id and
42 from the server's clock at receipt. 42 nothing else; the multiplier and payout come from the server's clock at
43 receipt.
43- Never send the crash point to the client before the round ends. A client that 44- Never send the crash point to the client before the round ends. A client that
44 knows it can cash out at `crash - epsilon` every round for a guaranteed win. 45 knows it can cash out at `crash - epsilon` every round for a guaranteed win.
45- The crash point is `max(1.0, (1 - edge) / (1 - u))`; do not "fix" the 1% of 46- The crash point is `max(1.0, (1 - edge) / (1 - u))`; do not "fix" the 1% of
46 rounds that crash at 1.00x. That is where the house edge is charged on an 47 rounds that crash at 1.00x. That is where the house edge is charged on an
47 instant cashout, and it is why no cashout lockout is needed. 48 instant cashout, and it is why no cashout lockout is needed.
49- Each round draws a 32-byte seed. `commit = sha256(seed)` goes out with
50 `CRASH_START`; the seed itself is revealed only when the round ends. The
51 crash point is `crash_point_from_seed(seed)` (first eight bytes, big-endian,
52 over 2^64), so a client can recompute it. Never reveal the seed while the
53 round is live. The guarantee is "fixed before you acted", not "ungrindable";
54 see ADR 0004.
48- The crash event is a server-initiated line (`CRASH_BUSTED`), not a reply. 55- The crash event is a server-initiated line (`CRASH_BUSTED`), not a reply.
49 Client code must drain the socket on a timer and route pushed lines by 56 Client code must drain the socket on a timer and route pushed lines by
50 prefix instead of assuming the next line answers the last request. 57 prefix instead of assuming the next line answers the last request.
51- `ServerCore::poll` must be idempotent: it settles the round, removes the 58- `ServerCore::poll` must be idempotent: it settles the round, removes the
52 session, and pushes once. A second poll must produce nothing. 59 session, and pushes once. A second poll must produce nothing.
53 60
61## Protocol v5
62- Every packet is `[u32 big-endian length][UTF-8 payload]`, at most 64 KiB.
63 Never read a declared length above the limit into memory; close instead.
64- Payloads are `REQ <id> [idem=<token>] <command>`, `RES <id> <body>` and
65 `EVT <seq> <body>`. The command/body text grammar is unchanged.
66- `ClientCore` allocates request ids. Adapters must return a reply with
67 `handle_reply_id`; a reply with an unknown id is ignored, never applied to
68 whatever request happens to be pending.
69- The server caches the last 256 request ids **per connection** and replays a
70 matching reply. A reused id with a different payload is an error. Do not make
71 this durable without a new ADR.
72- `HELLO` and `PONG` carry Unix milliseconds. The offset is for display and
73 interpolation; the server settles crash. Never derive a payout from a
74 browser or gateway clock.
75- While a crash round runs, `poll` pushes at most one `crash.tick` per 100 ms
76 per session. Settlement (`CRASH_BUSTED`) is never dropped or replaced by a
77 tick.
78- A wire change is a MAJOR bump. Do not add a compatibility mode for v4.
79
80## Web gateway (`webd/`)
81- The browser is a frontend, not a client. It sends `Command` JSON and renders
82 `View` JSON. It must never parse the game wire, compute a payout, or hold a
83 credential.
84- `webd/` is a separate workspace member. Its dependencies (tungstenite,
85 serde_json, cookie handling) must not enter the TUI, GUI or server build
86 graph.
87- One session thread owns one `ClientCore` and one upstream TLS connection.
88 No async runtime, no browser TOFU: `ZAGAN_UPSTREAM_FINGERPRINT` is required
89 and a mismatch refuses to connect.
90- The session cookie is HttpOnly, SameSite=Strict, and Secure on HTTPS. Never
91 store a password or credential in the browser or gateway. Session-only auth;
92 no remember-me without a new ADR.
93- Close is not forfeit: the gateway holds upstream for `ZAGAN_GRACE_SECONDS`
94 (default 60). Expiry closes upstream, which is the server's forfeit. A new
95 socket with the same cookie takes over; tell the old tab it was replaced.
96- Bind to loopback and let nginx terminate TLS. Check `Origin` against
97 `ZAGAN_WEB_ORIGIN`. `/ws` is the only gateway route; `/play/` is static.
98- A gateway restart loses live sessions and forfeits their rounds. Do not add
99 protocol-level resume here without an ADR.
100
101## Web app (`web/play/`)
102- Vanilla TypeScript + esbuild. No framework runtime, no game rules, no wire
103 protocol. DOM for the shell, canvas for the game surface.
104- Colours and spacing come from `web/play/style.css`, which mirrors
105 `gui/src/theme.rs`. Felt green is only ever the roulette table.
106- Build with `cd web/play && npm ci && npm run build`; commit the generated
107 `app.js` so the site needs no Node at deploy time.
108- `/play/` is the app; `/` stays the download page. Web v1 is play-money only:
109 no deposits, withdrawals or KYC copy.
110- The browser cookie covers refresh and the grace window only. A closed tab
111 that returns later logs in again.
112
54## GUI 113## GUI
55- The GUI is a second frontend over `ClientCore`, not a second client. 114- The GUI is a second frontend over `ClientCore`, not a second client.
56 Frontends send `Command` and render `View`; the core owns navigation and 115 Frontends send `Command` and render `View`; the core owns navigation and