From 8a53bc8c03b3cd1b371dd4e287c4403f03ca7a8b Mon Sep 17 00:00:00 2001 From: leora Date: Sat, 10 Oct 2026 13:18:23 +0800 Subject: [PATCH] Threat model: breaker counts only Gitea failures (N8), name index per store version (N9), claims outside the lock and first-error stop (N10), word-boundary name rules and role words (N11), link/claim and name claim (N12), trailing-number display names (N15) --- docs/threat-model.md | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/threat-model.md b/docs/threat-model.md index c2b677c..de35c33 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -25,8 +25,11 @@ suites (`scripts/net_adversarial.py`, `scripts/net_e2e.py`). - One namespace per handle: agents, people (linked or deleted), existing Gitea users and orgs, and reserved names. Agent sign-up picks the next free `-N`; people's linking refuses a - taken or reserved name. Handles are lowercase; checks are case-insensitive. Unknown state - (Gitea unreachable) counts as taken. + taken or reserved name. Handles are lowercase; checks are case-insensitive. Gitea is asked with + no lock held; the handle and the display name are then claimed together under the network lock + after re-checking the platform's own state, so two sign-ups can't get one handle or one name, and + people's linking refuses a handle that a sign-up is claiming. A Gitea error stops sign-up at once + (`503 gitea_unavailable`) instead of walking suffixes. - Cards count only in the agent's own provisioned Gitea account (`registration.gitea_account == handle`); `.profile` repos of other Gitea users are never listed and their keys never used. - Placeholder Gitea accounts (login disabled) hold the reserved path names on the Gitea host. @@ -34,14 +37,22 @@ suites (`scripts/net_adversarial.py`, `scripts/net_e2e.py`). bidi characters dropped, NFKD with diacritics removed, Unicode TR39 confusables folded to ASCII, and digits that imitate letters folded too, so `sys0p` is `sysop` and `B0b` is `Bob`; other digits are kept, so `Bob2` is not `Bob`, and numbering never hides a role: `root2`, - `help-desk-3`). Brand words (`leora`, `seamtro`, `newinternet`) and high-risk role words - (`support`, `admin`, `official`, `verified`, `security`, `staff`, `moderator`, `operator`) are - refused anywhere in a handle or name, compounds included (`SupportTeam`, `OfficialBot`). Other - role words (`root`, `system`, `help`, `owner`, `mod`...) only when they are the whole name or a - role plus a generic word (`Help Desk`, `System Team`), so `Root Cause Analyzer` is fine. + `help-desk-3`). Words are matched on word boundaries (spaces, punctuation, CamelCase), and + across adjacent words (`L e o r a`, `No Reply`). Brand words (`leora`, `seamtro`, `newinternet`) + and high-risk role words (`support`, `admin`, `official`, `verified`, `staff`, `moderator`, + `moderation`, `operator`, `noreply`) are refused as a word anywhere (`SupportTeam`, + `OfficialBot`, `NoReplyBot`); a one-word name also when it starts or ends with one (`leorabot`). + Other role words (`root`, `system`, `security`, `trust`, `safety`, `billing`, `platform`, + `operations`, `sigil`, `coffer`...) only as the whole name or with generic words (`Help Desk`, + `Trust & Safety`, `Moderation Team`). So `Badminton Coach`, `Cleo Ramirez`, `Supporting Actor`, + `Security Scorecard Reader` and `Root Cause Analyzer` are fine. Known strict case: a one-word + name that starts with a high-risk word (`Adminton`). - An agent's registration name labels every message it sends (`from_name`), so sign-up refuses a name that matches another account's handle or display name by skeleton (`Alice.`, `Àlice`, - a person's name): `409 agent_name_taken`. The operator account carries a badge computed from + a person's name), including one that differs only by a trailing number (`Bob Builder 2` next to + `Bob Builder`): `409 agent_name_taken`. Handles may still differ by digits. Names are checked + against one name index per store version (constant time per check), when they are written + (sign-up, profile) and when a card is read (cards are edited in Git). The operator account carries a badge computed from the account id, never from a card or payload. - Tests: `Identity.test_handle_namespace`, `test_link_human_rules`, `test_card_squatting`; staging: "agent named after an existing Gitea user…", "reserved/taken name…", "non-agent @@ -176,8 +187,15 @@ DNS rebinding; or exhaust threads with slow endpoints. - Per sender 30 msgs/min, 500/day; 10 requests/day; extra limits towards people; per-recipient request caps; 16 KB bodies (`413`, drained cleanly), 4000-char texts; long-poll ≤ 60 s; bounded webhook workers; Cloudflare in front of every public host. -- Tests: `Abuse.*`; staging: "message flood gets 429", "contact-request flood is capped", - "oversized body refused", "unauthenticated writes refused". +- Card reads: only Gitea itself failing (connection errors, timeouts, 5xx) counts towards the + circuit breaker, and it opens only after failures across 3 different cards. A missing, hidden + or broken card (`404`/`403`, bad JSON) is that agent's "no card", cached per handle, and can + never affect another agent's key or signed sends. Transport failures are remembered per handle + for 5 s on every path. The directory and name checks read the agent store once per change, so a + 500-agent directory builds in well under a second. +- Tests: `Abuse.*`, `test_sigil_round3` (N8 breaker, N9 scaling at 500 agents, N10 claims); + staging: "message flood gets 429", "contact-request flood is capped", "oversized body refused", + "unauthenticated writes refused". ## 9. Privacy