MAS-WP-0005-T03: match ticket ids case-insensitively (SCP-7)
Forwarders (IONOS) and list servers (Google Groups) lower-case local-parts, which lost a list post to its ticket in MAS-WP-0003. Lookups use COLLATE NOCASE; storage and replies keep the id as issued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 252470@bnt-lap001 Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
This commit is contained in:
parent
c0c02e9a9f
commit
12d7e972ba
3 changed files with 35 additions and 7 deletions
|
|
@ -9,6 +9,9 @@ proves the canonical lifecycle:
|
|||
C closed ticket=ID@DOMAIN (closed) -> archived, ticket not mutated, one notice
|
||||
D unknown ticket=NOPE@DOMAIN -> quarantined, no reply (no backscatter)
|
||||
|
||||
Ticket ids match case-insensitively (`ticket=gs-id@` finds `GS-ID`), because
|
||||
forwarders and mailing lists lower-case local-parts in practice.
|
||||
|
||||
Resource addresses are correlation identifiers, not credentials: knowing
|
||||
one lets you send mail to the ticket, nothing more. Admin endpoints here are
|
||||
unauthenticated and meant for local experiments only.
|
||||
|
|
@ -106,12 +109,14 @@ class TicketService:
|
|||
"requested_interaction": honored}
|
||||
|
||||
def _to_resource(self, event, mas, msg):
|
||||
tid = mas["resource_id"]
|
||||
row = self.db.execute("SELECT * FROM tickets WHERE id=? AND domain=?",
|
||||
(tid, mas["domain"])).fetchone()
|
||||
# Ids match case-insensitively: forwarders and list servers lower-case
|
||||
# local-parts (SCP-7). From here on use the id as issued.
|
||||
row = self.db.execute("SELECT * FROM tickets WHERE id=? COLLATE NOCASE AND domain=?",
|
||||
(mas["resource_id"], mas["domain"])).fetchone()
|
||||
if row is None:
|
||||
self._store(None, "quarantined-unknown", event, mas, msg)
|
||||
return 202, {"status": "unknown-resource", "action": "quarantined, no reply"}
|
||||
tid = row["id"]
|
||||
if row["status"] == "closed":
|
||||
self._store(tid, "archived-after-close", event, mas, msg)
|
||||
notified = False
|
||||
|
|
@ -169,13 +174,15 @@ class TicketService:
|
|||
|
||||
def close(self, tid: str) -> bool:
|
||||
with self.lock, self.db:
|
||||
return self.db.execute("UPDATE tickets SET status='closed' WHERE id=?", (tid,)).rowcount == 1
|
||||
return self.db.execute("UPDATE tickets SET status='closed' WHERE id=? COLLATE NOCASE",
|
||||
(tid,)).rowcount == 1
|
||||
|
||||
def ticket(self, tid: str) -> Optional[dict]:
|
||||
row = self.db.execute("SELECT * FROM tickets WHERE id=?", (tid,)).fetchone()
|
||||
row = self.db.execute("SELECT * FROM tickets WHERE id=? COLLATE NOCASE", (tid,)).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
msgs = self.db.execute("SELECT * FROM messages WHERE ticket_id=? ORDER BY seq", (tid,)).fetchall()
|
||||
msgs = self.db.execute("SELECT * FROM messages WHERE ticket_id=? ORDER BY seq",
|
||||
(row["id"],)).fetchall()
|
||||
return {**dict(row), "messages": [dict(m) for m in msgs]}
|
||||
|
||||
def outbox(self) -> list[dict]:
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ def test_canonical_lifecycle(env):
|
|||
assert len(svc.outbox()) == before and len(svc.quarantine()) == 1
|
||||
|
||||
|
||||
def test_lower_cased_id_correlates(env):
|
||||
# IONOS forwards and Google Groups members arrive lower-cased (MAS-WP-0003 T04)
|
||||
svc, send, *_ = env
|
||||
send(f"ticket!callback@{D}", "request")
|
||||
d = send(f"ticket=gs-41872+comments@{D}", "via a mailing list")
|
||||
assert d.response == {"status": "correlated", "ticket_id": "GS-41872", "facet": "comments"}
|
||||
assert len(svc.ticket("gs-41872")["messages"]) == 2
|
||||
assert svc.close("gs-41872")
|
||||
d = send(f"ticket=gs-41872@{D}", "after close")
|
||||
assert d.response["ticket_id"] == "GS-41872" and d.response["notified"] is True
|
||||
assert svc.outbox()[-1]["mail_from"] == f"ticket=GS-41872@{D}" # replies use the issued id
|
||||
|
||||
|
||||
def test_send_cap_keeps_replies_in_outbox(env):
|
||||
svc, send, *_ = env
|
||||
sent = []
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fixtures and update the parser, policy and schema.
|
|||
|
||||
```task
|
||||
id: MAS-WP-0005-T03
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "7c19e951-7ab4-580c-9e4c-9fead9ee946e"
|
||||
```
|
||||
|
|
@ -62,6 +62,14 @@ and a list post was lost to its ticket. Make the reference ticket service
|
|||
issue and match ids case-insensitively. Crockford base32 is case-insensitive
|
||||
by design. Verify on the live MX by repeating the Google Groups post.
|
||||
|
||||
- 2026-10-18: implemented. Ticket lookup uses `COLLATE NOCASE` (also in the
|
||||
`ticket()`/`close()` admin calls). Once found, the service stores,
|
||||
replies and reports under the id as issued, so replies keep coming From
|
||||
`ticket=GS-…@`. Test `test_lower_cased_id_correlates` passes, as do all
|
||||
137 tests. Deployed as `mas-live:wp0005-20261018`. A dry check on a copy
|
||||
of the live DB resolves `gs-85qj8wc09p` to `GS-85QJ8WC09P`. Pending:
|
||||
live Google Groups post.
|
||||
|
||||
## v0.2 drafts: SCP-6 and the SCP-7 statute field
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue