summaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
Diffstat (limited to 'AGENTS.md')
-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