-
-
Notifications
You must be signed in to change notification settings - Fork 3
Revise PR #283: taosmd/mentions.py is added by BOTH this PR and #233, and here it has no caller #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Revise PR #283: taosmd/mentions.py is added by BOTH this PR and #233, and here it has no caller #304
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ### Fixed: identity comparison and revocation check normalised | ||
|
|
||
| - Fixed `taosmd/registry_auth.py:authorize_sender` to normalise handle identity | ||
| using :func:`taosmd.service._normalise_handle` in both the ``sub != claimed_from`` | ||
| comparison (line 83) and the revocation check (line 92), so ``@``-prefixed and | ||
| bare handles compare equal case-insensitively across both code paths. | ||
|
|
||
| ### Added: taosmd/mentions.py | ||
|
|
||
| - Created `taosmd/mentions.py` with `MentionStore` class for recording and | ||
| querying ``@``-handle mentions on the A2A bus, enabling the | ||
| ``/a2a/mentions`` feed and thread-scoped visibility anti-bypass rule from | ||
| taOSmd ``#211``. The store normalises handles via `:func:`_normalise_handle`` | ||
| so write and read paths agree on handle spelling. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| """Mention index for the A2A bus. | ||
|
|
||
| Stores @handle mentions extracted from A2A message bodies and the optional | ||
| ``recipient`` field, enabling the /a2a/mentions feed and thread-scoped | ||
| visibility (anti-bypass rule from taOSmd #211). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import sqlite3 | ||
|
|
||
| from taosmd import _db | ||
| from taosmd.service import _normalise_handle | ||
|
|
||
| _MENTION_RE = re.compile(r'@([a-zA-Z0-9_-]+)') | ||
|
|
||
|
|
||
| class MentionStore: | ||
| """Append-only mention index keyed by (mentioned_handle, message_id, ts, thread).""" | ||
|
|
||
| def __init__(self, db_path: str) -> None: | ||
| self._db_path = db_path | ||
| self._conn: sqlite3.Connection | None = None | ||
|
|
||
| async def init(self) -> None: | ||
| self._conn = _db.connect(self._db_path) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Connection leak in
Wrap the assignment and Reply with |
||
| self._conn.executescript(""" | ||
| CREATE TABLE IF NOT EXISTS mentions ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| mentioned_handle TEXT NOT NULL, | ||
| message_id INTEGER NOT NULL, | ||
| ts REAL NOT NULL, | ||
| thread TEXT NOT NULL | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_mentions_handle_ts | ||
| ON mentions(mentioned_handle, ts); | ||
| """) | ||
| self._conn.commit() | ||
|
|
||
| async def close(self) -> None: | ||
| if self._conn: | ||
| self._conn.close() | ||
| self._conn = None | ||
|
|
||
| async def record_mentions( | ||
| self, | ||
| message_id: int, | ||
| body: str, | ||
| thread: str, | ||
| ts: float, | ||
| recipient: str | None = None, | ||
| ) -> None: | ||
| handles: set[str] = set() | ||
| for m in _MENTION_RE.finditer(body or ''): | ||
| handles.add(_normalise_handle(m.group(1), mint_strip=True)) | ||
| if recipient: | ||
| handles.add(_normalise_handle(recipient, mint_strip=True)) | ||
|
|
||
| for handle in handles: | ||
| self._conn.execute( | ||
| "INSERT INTO mentions (mentioned_handle, message_id, ts, thread) VALUES (?, ?, ?, ?)", | ||
| (handle, message_id, ts, thread), | ||
| ) | ||
| if handles: | ||
| self._conn.commit() | ||
|
|
||
| async def get_mentioned_message_ids( | ||
| self, reader: str, since: float | None = None, limit: int = 50 | ||
| ) -> list[dict]: | ||
| reader = _normalise_handle(reader, mint_strip=True) | ||
| query = "SELECT message_id, ts FROM mentions WHERE mentioned_handle = ?" | ||
| params: list = [reader] | ||
| if since is not None: | ||
| query += " AND ts > ?" | ||
| params.append(since) | ||
| query += " ORDER BY ts ASC LIMIT ?" | ||
| params.append(limit) | ||
| rows = self._conn.execute(query, params).fetchall() | ||
| return [{"message_id": r[0], "ts": r[1]} for r in rows] | ||
|
|
||
| async def get_mention_recipients(self, message_id: int) -> list[str]: | ||
| rows = self._conn.execute( | ||
| "SELECT mentioned_handle FROM mentions WHERE message_id = ?", | ||
| (message_id,), | ||
| ).fetchall() | ||
| return [r[0] for r in rows] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| import hashlib | ||
| import json | ||
| import logging | ||
|
|
@@ -41,6 +42,27 @@ | |
| _remote_cache: dict[tuple[str, str | None], object] = {} | ||
|
|
||
|
|
||
| def _normalise_handle(handle: str, *, mint_strip: bool = False) -> str: | ||
| """Slug-match helper for identity comparison on the A2A bus. | ||
|
|
||
| This is a slug match, NOT an identity check: two distinct agents that | ||
| share a stem will unify under this function. Do not use it for | ||
| authorisation decisions that require distinguishing between same-stem | ||
| identities. | ||
|
|
||
| Strips any leading ``@``, casefolds, and optionally strips a timestamp | ||
| mint stamp (``-YYYYMMDD`` or ``-YYYYMMDD-HHMMSS`` suffix) when | ||
| ``mint_strip=True``. The default is ``mint_strip=False`` because the | ||
| ``from`` field on bus messages does not carry bare-or-@ twins for the | ||
| same canonical, and stripping would collapse install discriminators | ||
| such as ``@taOS-agent-1a2b3c4d``. | ||
| """ | ||
| handle = handle.lstrip("@").casefold() | ||
| if mint_strip: | ||
| handle = re.sub(r"-\d{8}(-\d{6})?$", "", handle) | ||
| return handle | ||
|
|
||
|
|
||
| def _get_remote(data_dir=None): | ||
| """Return a cached :class:`~taosmd.remote.RemoteClient` when a server URL | ||
| is configured, otherwise ``None`` (use local path). | ||
|
|
@@ -442,6 +464,12 @@ async def a2a_send( | |
| ) | ||
| stores = await _api._ensure_stores(data_dir) | ||
| archive = stores["archive"] | ||
| from taosmd.mentions import MentionStore # noqa: PLC0415 | ||
| db_path = str(Path(data_dir) / "mentions.db") if data_dir else None | ||
| mention_store: MentionStore | None = None | ||
| if db_path is not None: | ||
| mention_store = MentionStore(db_path) | ||
| await mention_store.init() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING:
Add a try/finally (or async context manager) around the archive write and mention recording to guarantee Reply with |
||
| # Redirect sends to renamed channels: if the target thread has been aliased | ||
| # to a new name, route the message to the canonical name instead. | ||
| if data_dir is not None: | ||
|
|
@@ -460,6 +488,16 @@ async def a2a_send( | |
| app_id=thread, | ||
| summary=body[:200], | ||
| ) | ||
| if mention_store is not None: | ||
| await mention_store.record_mentions( | ||
| message_id=row_id, | ||
| body=body, | ||
| thread=thread, | ||
| ts=row_id, | ||
| recipient=None, | ||
| ) | ||
| if mention_store is not None: | ||
| await mention_store.close() | ||
| receipt = {"id": row_id, "from": sender, "thread": thread, "reply_to": reply_to} | ||
| if refs is not None: | ||
| receipt["refs"] = refs | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """Test that identity comparison uses _normalise_handle.""" | ||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from taosmd.service import _normalise_handle | ||
|
|
||
|
|
||
| class TestNormaliseHandle: | ||
| def test_strips_at_prefix(self): | ||
| assert _normalise_handle("@taos-agent") == "taos-agent" | ||
|
|
||
| def test_case_fold(self): | ||
| assert _normalise_handle("@TaOS-Agent") == "taos-agent" | ||
| assert _normalise_handle("TAOS-AGENT") == "taos-agent" | ||
|
|
||
| def test_same_handle_no_prefix(self): | ||
| assert _normalise_handle("taos-agent") == "taos-agent" | ||
|
|
||
| def test_different_handles_still_differ(self): | ||
| assert _normalise_handle("taos-agent") != _normalise_handle("other-agent") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test