diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 20:54:28 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 20:54:28 +0200 |
| commit | b93cd918e967c147b6169fa5bb89241bd7488525 (patch) | |
| tree | 578c0b632db5110738b93f94a87e71b6632b61ec /_docs/chat-contract.md | |
| parent | 577c9241a4ac1c55c5ed0031b76a8a1f83798752 (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.md | 42 |
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 |