aboutsummaryrefslogtreecommitdiff
path: root/_docs/chat-contract.md
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 20:54:28 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 20:54:28 +0200
commitb93cd918e967c147b6169fa5bb89241bd7488525 (patch)
tree578c0b632db5110738b93f94a87e71b6632b61ec /_docs/chat-contract.md
parent577c9241a4ac1c55c5ed0031b76a8a1f83798752 (diff)
docs: chat bridge in AGENTS.md, changelog v4.6.72-80, contract deltas
Contract now documents truncated/cursor semantics, the full code table with retry rules, 503-from-nginx (HTML, retryable), the two-cursor rule, strictly-increasing epoch, and the security posture in plain words: client-minted tokens mean public write, mitigated but not closed.
Diffstat (limited to '_docs/chat-contract.md')
-rw-r--r--_docs/chat-contract.md42
1 files changed, 36 insertions, 6 deletions
diff --git a/_docs/chat-contract.md b/_docs/chat-contract.md
index 6f94d79..aa99b0c 100644
--- a/_docs/chat-contract.md
+++ b/_docs/chat-contract.md
@@ -42,12 +42,15 @@ Response `200`:
{"success": true, "thread": "a1b2c3d4e5f60718",
"messages": [{"id": "MSG-…", "epoch": 1792778400123, "ts": "2026-10-23T18:40:00+00:00", "author": "operator", "text": "…"}],
"awaiting": true,
+ "truncated": false,
"cursor": 1792778400123}
```
- `awaiting` = the last message in the thread was **not** written by `hermes`. There is no unread
counter: the server keeps no read state, so a counter could never clear.
-- `cursor` = epoch of the newest message in the thread. Clients store it and pass it as `since`.
+- `cursor` = epoch of the **last message actually returned**. With more than 50 new messages the
+ response is cut to 50 (`truncated: true`) and the cursor stops at the cut — returning the
+ thread's real tail would skip everything in between, permanently.
**The cursor is epoch, never `ts`.** `ts` carries an offset (`+00:00`), and comparing strings with
mixed offsets silently drops messages in the same second and reorders others.
@@ -64,17 +67,44 @@ mixed offsets silently drops messages in the same second and reorders others.
| Code | When | Body |
|---|---|---|
-| `401` | token missing or not 64 hex | `{"success": false, "error": "missing or malformed X-Chat-Token"}` |
-| `413` | body > 32 KB, or text > 4000 chars | `{"success": false, "error": "text too long (max 4000 chars)"}` |
-| `429` | 20 messages per thread / 40 per IP per 10 min | `{"success": false, "error": "rate limit…"}` |
-| `507` | more than 500 threads on the host | `{"success": false, "error": "thread limit reached"}` |
-| `405` | wrong method | `{"success": false, "error": "Method not allowed"}` |
+| Status | `code` | Retry? |
+|---|---|---|
+| `401` | `bad_token` | no — `hermes-reset` |
+| `413` | `too_long` (4000 codepoints) / `too_large` (32 KB body) | no — shorten |
+| `429` | `rate_limited` + `retry_after_ms`, `scope`, `limit`, `window_s`, and a `Retry-After` header | yes, after the wait |
+| `507` | `thread_limit` | no |
+| `405` | `method` | no |
+| `500` | `thread file is corrupt` | no — the file is left untouched; a corrupt thread is never silently reset |
+| `503` | *(nginx, HTML body)* | **yes** — the front-end rate zone (`60 r/min`, burst 50) fired before the app. Back off; do not parse it as JSON. |
+
+Every body is `{"success": false, "code": "…", "error": "…", …}`. A client may retry on
+`429` and `503` only — everything else is permanent for that payload.
Rules that follow from this table:
- **Auth is checked before anything touches disk.** A `401` means no file was opened.
- **Text is rejected, never truncated.** A shortened question gets a wrongly scoped answer.
- Rate limits are two windows, per thread **and** per IP: a token is mintable in a loop, an IP is not.
+- **Two readers, two cursors.** The terminal's 30 s poller and the agent's `chat_hermes read` read
+ the same thread but store separate cursors client-side (`webx11_chat_state.cursor` for display,
+ `webx11_chat_agent_cursor_<thread>` for the tool). Sharing one made the poller consume the reply
+ before the model could see it.
+- **`epoch` is strictly increasing.** Two posts in the same millisecond get `max(now, last+1)`, so
+ a `since` reader never loses the second one.
+
+## Security posture
+
+The token is minted by the client, so this channel is **public write by design** — there is no
+login. Anyone who can reach the endpoint can post. Consequences, deliberately accepted:
+
+- The auto-reply agent treats message text as **untrusted data**: it may read files to check a
+ claim, but it never runs a command or writes anything because a message asked it to.
+- Bounded by 20/thread, 40/IP per 10 min, 500 threads, 200 messages per thread, 30 days.
+- `data/chat/` is not reachable over HTTP (`403`); thread ids are `sha256(token)[0:16]`, so there is
+ nothing to enumerate and no id in any URL.
+
+Closing this properly needs platform-session auth, which the browser app does not have. Do not
+bolt on half-auth: it moves the hole without closing it.
## Retention