Skip to content

refactor(browser): split BrowserStore into per-domain mixins (B4)#554

Merged
jaylfc merged 1 commit into
devfrom
refactor/browser-store-mixins
Jun 3, 2026
Merged

refactor(browser): split BrowserStore into per-domain mixins (B4)#554
jaylfc merged 1 commit into
devfrom
refactor/browser-store-mixins

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Jun 3, 2026

Modularity plan B4. Splits the 1204-line BrowserStore god-class in routes/desktop_browser/store.py into 9 per-domain mixins under store_mixins/ (profiles, windows, history, bookmarks, pins, capabilities, drive_sessions, site_permissions, push). Methods copied verbatim; BrowserStore now composes the mixins with BaseStore LAST in the bases (so mixin methods take precedence and BaseStore provides __init__/helpers via MRO). SCHEMA = BROWSER_SCHEMA and the BrowserCookieStore re-export preserved.

store.py 1204 → 37 lines. All 45 methods confirmed still callable on BrowserStore; no circular import. Full desktop_browser suite (560 tests) passes.

Summary by CodeRabbit

  • Refactor
    • Reorganized browser storage architecture for improved system maintainability and scalability

The internal structure of the browser store has been restructured into modular components handling bookmarks, history, profiles, windows, agent permissions, push notifications, and site settings. No changes to user-facing functionality.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

BrowserStore refactors from a monolithic 1200+-line class into nine focused mixins (Profiles, Windows, History, Bookmarks, Pins, Capabilities, DriveSession, SitePermissions, Push) plus BaseStore composition. Each mixin provides database-backed CRUD and query operations for its domain while the store coordinates these behaviors.

Changes

Store mixin composition refactoring

Layer / File(s) Summary
Store composition and wiring
tinyagentos/routes/desktop_browser/store.py
BrowserStore is redefined to inherit from nine mixins plus BaseStore, moving implementation details from the main file into focused modules. Imports are added for each mixin, and BrowserCookieStore re-export is maintained for backward compatibility.
User profiles, windows, and history
tinyagentos/routes/desktop_browser/store_mixins/profiles.py, store_mixins/windows.py, store_mixins/history.py
ProfilesMixin provides profile CRUD (with last-profile deletion guard), claim-initialization races, and updates. WindowsMixin persists browser window state by user. HistoryMixin records and searches visit history with substring matching. All validate scopes and commit transactions.
Bookmarks and pins
tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py, store_mixins/pins.py
BookmarksMixin implements full lifecycle (create with auto-generated ID, list with search, find by URL, delete). PinsMixin manages capped agent pins per tab with detailed return states (duplicate/at\_cap/inserted), list by tab/user, and count. Both track and order by temporal metadata.
Capabilities and site permissions
tinyagentos/routes/desktop_browser/store_mixins/capabilities.py, store_mixins/site_permissions.py
CapabilitiesMixin manages agent capability grants with optional expiration, permission checking against wildcard host patterns, and convenience checks. SitePermissionsMixin manages per-site allow/deny grants with pattern precedence (exact > subdomain > wildcard) and revoke operations.
Drive session lifecycle
tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py
DriveSessionsMixin tracks agent activity: start initiates, bump updates last\_op\_at, end removes, is\_driving checks idle status with timeout threshold, and prune\_expired\_drive\_sessions cleans stale rows. All scoped to (user\_id, profile\_id, tab\_id, agent\_id).
Push subscriptions and mutes
tinyagentos/routes/desktop_browser/store_mixins/push.py
PushMixin manages Web Push subscriptions by (user\_id, device\_id) with endpoint deduplication logic, and per-agent mute state (mention, reply, etc.). Subscriptions and mutes are independently persisted with set/list/check operations and kind validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jaylfc/tinyagentos#307: Refactors drive-session and capability enforcement logic that depends on the new CapabilitiesMixin and DriveSessionsMixin extracted in this PR.
  • jaylfc/tinyagentos#300: Introduced the original BrowserStore and per-user/profile schema foundation that these mixins decompose and extend.
  • jaylfc/tinyagentos#311: Implements Web Push routes and endpoints that directly use the new PushMixin subscription and mute methods extracted here.

Poem

🐰 One store grew large, a towering tree,
So we split its limbs for all to see—
Profiles, bookmarks, pins, and more,
Each mixin a door to its own wee floor.
Compose them together, the whole works anew,
A refactored warren, through and through! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the primary change: refactoring BrowserStore into per-domain mixins, which is the main objective. It avoids vague language and accurately reflects the substantial architectural shift in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/browser-store-mixins

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

if not user_id or not profile_id:
raise ValueError("user_id and profile_id required")
assert self._db is not None
like = f"%{query}%"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The search query is used to build a LIKE pattern with wildcards on both sides. This allows the user to use % and _ as wildcards in their search, which may be unintended if literal matching is expected. Consider escaping the user's input or documenting the behavior.

await self._db.execute(
"INSERT OR REPLACE INTO site_permissions "
"(user_id, profile_id, host_pattern, permission, state, granted_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The host_pattern is not validated when setting a site permission. An invalid pattern (e.g., containing multiple asterisks) may lead to unexpected matching behavior. Consider validating that the pattern is either '', or starts with '.' followed by a string that does not contain any asterisks.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
tinyagentos/routes/desktop_browser/store_mixins/push.py (1)

152-162: 💤 Low value

Consider adding validation for consistency.

This method skips validation unlike other methods in the mixin. While it's a read-only operation (so invalid inputs just return False), validating kind against _PUSH_MUTE_KINDS would catch caller bugs early—especially since set_push_mute validates it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/push.py` around lines 152 -
162, The is_push_muted method lacks the same validation other mixin methods
perform; add a check at the start of is_push_muted to validate the kind argument
against _PUSH_MUTE_KINDS (same way set_push_mute does) and raise a ValueError
(or assert) on invalid kinds, so callers get immediate feedback about bad inputs
before the DB query; reference the is_push_muted function and the
_PUSH_MUTE_KINDS constant when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py`:
- Around line 29-57: In list_bookmarks ensure limit is validated and enforced
before running the SQL: check that the limit parameter is an int > 0 (and
optionally clamp to a max like 1000) and raise ValueError for non-positive
values; then use the validated/clamped value in the SQL parameter passed to
self._db.execute so callers cannot pass a negative value that turns into an
unbounded SQLite LIMIT.

In `@tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py`:
- Line 79: Validate idle_timeout_s at the start of any method that uses it for
cutoff arithmetic (e.g., prune_expired_drive_sessions and any other method that
computes cutoff using idle_timeout_s): add a guard that requires idle_timeout_s
> 0 and return early (no-op) or raise a clear error if the value is non-positive
to avoid producing a future cutoff and accidentally pruning active sessions;
ensure the check is applied before computing cutoff = now - idle_timeout_s so
all timeout-based logic is protected.

In `@tinyagentos/routes/desktop_browser/store_mixins/history.py`:
- Around line 31-46: The method performing the history lookup currently forwards
the `limit` directly into the SQL LIMIT, so pass a negative or zero `limit` can
produce an unbounded query; update the function that has parameters (query: str,
limit: int = 8) to validate `limit` before calling self._db.execute: if limit <=
0 raise a ValueError (e.g., "limit must be positive"), and optionally clamp
overly large values to a safe maximum before building the `like` and calling
self._db.execute (refer to the `limit` parameter and the `_db.execute` call in
this function).

In `@tinyagentos/routes/desktop_browser/store_mixins/pins.py`:
- Around line 92-139: The read methods skip the same required-id validation that
add_pin/delete_pin perform; update list_pins_for_tab and count_pins_for_tab to
call the same validation helpers for user/profile/tab (e.g.,
self._require_user_id(user_id), self._require_profile_id(profile_id),
self._require_tab_id(tab_id)) at the top of each method, and update
list_pins_for_user to call self._require_user_id(user_id) before DB access,
leaving the rest of the logic unchanged.
- Around line 36-90: In add_pin_if_under_cap, validate the max_pins argument up
front and raise a clear error for non-positive values (e.g., if max_pins <= 0:
raise ValueError("max_pins must be a positive integer")), so callers fail fast
instead of silently returning "at_cap"/"duplicate"; add this guard near the
existing user_id/profile_id/tab_id/agent_id checks before using max_pins in the
INSERT SELECT.

In `@tinyagentos/routes/desktop_browser/store_mixins/push.py`:
- Around line 113-122: In set_push_mute validate that user_id and agent_id are
non-empty strings (same style used in delete_push_subscription) before any DB
operations: check user_id and agent_id and raise ValueError with a clear message
if either is missing/empty, then proceed to validate kind against
_PUSH_MUTE_KINDS and perform the insert/delete; reference the set_push_mute
method and mirror the parameter validation used elsewhere in this mixin to
prevent inserting rows with empty identifiers.

---

Nitpick comments:
In `@tinyagentos/routes/desktop_browser/store_mixins/push.py`:
- Around line 152-162: The is_push_muted method lacks the same validation other
mixin methods perform; add a check at the start of is_push_muted to validate the
kind argument against _PUSH_MUTE_KINDS (same way set_push_mute does) and raise a
ValueError (or assert) on invalid kinds, so callers get immediate feedback about
bad inputs before the DB query; reference the is_push_muted function and the
_PUSH_MUTE_KINDS constant when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 789b6a3b-c503-49dd-93ac-adb1cb14d7e9

📥 Commits

Reviewing files that changed from the base of the PR and between b741f23 and 34b6657.

📒 Files selected for processing (11)
  • tinyagentos/routes/desktop_browser/store.py
  • tinyagentos/routes/desktop_browser/store_mixins/__init__.py
  • tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py
  • tinyagentos/routes/desktop_browser/store_mixins/capabilities.py
  • tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py
  • tinyagentos/routes/desktop_browser/store_mixins/history.py
  • tinyagentos/routes/desktop_browser/store_mixins/pins.py
  • tinyagentos/routes/desktop_browser/store_mixins/profiles.py
  • tinyagentos/routes/desktop_browser/store_mixins/push.py
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py
  • tinyagentos/routes/desktop_browser/store_mixins/windows.py

Comment on lines +29 to +57
async def list_bookmarks(
self,
*,
user_id: str,
profile_id: str,
query: str | None = None,
limit: int = 100,
) -> list[dict]:
"""Return bookmarks for (user, profile), optionally substring-filtered."""
if not user_id or not profile_id:
raise ValueError("user_id and profile_id required")
assert self._db is not None
if query:
like = f"%{query}%"
cursor = await self._db.execute(
"SELECT bookmark_id, folder_path, url, title, created_at "
"FROM bookmarks "
"WHERE user_id = ? AND profile_id = ? "
" AND (url LIKE ? OR title LIKE ?) "
"ORDER BY created_at DESC LIMIT ?",
(user_id, profile_id, like, like, limit),
)
else:
cursor = await self._db.execute(
"SELECT bookmark_id, folder_path, url, title, created_at "
"FROM bookmarks "
"WHERE user_id = ? AND profile_id = ? "
"ORDER BY created_at DESC LIMIT ?",
(user_id, profile_id, limit),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

How does SQLite interpret a negative LIMIT clause?

💡 Result:

In SQLite, when a LIMIT clause evaluates to a negative value, it is interpreted as "no limit" [1][2]. This means there is no upper bound on the number of rows returned by the query [1]. Consequently, the database will return all rows that meet the other criteria of the statement [1][2]. This behavior applies to both SELECT statements [1] and, when the SQLITE_ENABLE_UPDATE_DELETE_LIMIT compile-time option is enabled, DELETE statements [2]. In contrast, if an OFFSET clause evaluates to a negative value, SQLite treats it as if it were zero [1][2].

Citations:


Reject non-positive limit values in list_bookmarks

limit is passed directly into LIMIT ?; in SQLite, a negative LIMIT means “no limit”, so a bad caller can turn this into an unbounded scan. Validate limit > 0 (and optionally clamp to a reasonable max) before executing the query.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py` around lines 29
- 57, In list_bookmarks ensure limit is validated and enforced before running
the SQL: check that the limit parameter is an int > 0 (and optionally clamp to a
max like 1000) and raise ValueError for non-positive values; then use the
validated/clamped value in the SQL parameter passed to self._db.execute so
callers cannot pass a negative value that turns into an unbounded SQLite LIMIT.

profile_id: str,
tab_id: str,
agent_id: str,
idle_timeout_s: float = 30.0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate idle_timeout_s before time arithmetic to prevent accidental mass pruning.

A negative timeout makes cutoff a future timestamp, so prune_expired_drive_sessions can delete active sessions unexpectedly. Add an explicit non-negative guard (ideally strict > 0) at method entry for both timeout-based methods.

Proposed fix
 async def is_driving(
@@
-        idle_timeout_s: float = 30.0,
+        idle_timeout_s: float = 30.0,
     ) -> bool:
         """True iff a row exists AND (now - last_op_at) < idle_timeout_s."""
+        if idle_timeout_s <= 0:
+            raise ValueError("idle_timeout_s must be > 0")
         assert self._db is not None
@@
 async def prune_expired_drive_sessions(
@@
-        idle_timeout_s: float = 30.0,
+        idle_timeout_s: float = 30.0,
     ) -> int:
         """Atomically delete rows idle for >= idle_timeout_s. Returns rowcount."""
+        if idle_timeout_s <= 0:
+            raise ValueError("idle_timeout_s must be > 0")
         assert self._db is not None

Also applies to: 98-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py` at line
79, Validate idle_timeout_s at the start of any method that uses it for cutoff
arithmetic (e.g., prune_expired_drive_sessions and any other method that
computes cutoff using idle_timeout_s): add a guard that requires idle_timeout_s
> 0 and return early (no-op) or raise a clear error if the value is non-positive
to avoid producing a future cutoff and accidentally pruning active sessions;
ensure the check is applied before computing cutoff = now - idle_timeout_s so
all timeout-based logic is protected.

Comment on lines +31 to +46
query: str,
limit: int = 8,
) -> list[dict]:
"""Substring match on url + title for the (user, profile). Most-recent first."""
if not user_id or not profile_id:
raise ValueError("user_id and profile_id required")
assert self._db is not None
like = f"%{query}%"
cursor = await self._db.execute(
"SELECT url, title, visited_at "
"FROM history "
"WHERE user_id = ? AND profile_id = ? "
" AND (url LIKE ? OR title LIKE ?) "
"ORDER BY visited_at DESC "
"LIMIT ?",
(user_id, profile_id, like, like, limit),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-positive history limits.

limit is passed straight into LIMIT ?, so a caller can send a negative value and bypass the cap entirely. That turns this into an unbounded history query instead of the small bounded lookup the method advertises.

Suggested fix
     async def search_history(
         self,
         *,
         user_id: str,
         profile_id: str,
         query: str,
         limit: int = 8,
     ) -> list[dict]:
         """Substring match on url + title for the (user, profile). Most-recent first."""
         if not user_id or not profile_id:
             raise ValueError("user_id and profile_id required")
+        if limit <= 0:
+            raise ValueError("limit must be positive")
         assert self._db is not None
         like = f"%{query}%"
         cursor = await self._db.execute(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/history.py` around lines 31 -
46, The method performing the history lookup currently forwards the `limit`
directly into the SQL LIMIT, so pass a negative or zero `limit` can produce an
unbounded query; update the function that has parameters (query: str, limit: int
= 8) to validate `limit` before calling self._db.execute: if limit <= 0 raise a
ValueError (e.g., "limit must be positive"), and optionally clamp overly large
values to a safe maximum before building the `like` and calling self._db.execute
(refer to the `limit` parameter and the `_db.execute` call in this function).

Comment on lines +36 to +90
async def add_pin_if_under_cap(
self,
*,
user_id: str,
profile_id: str,
tab_id: str,
agent_id: str,
max_pins: int,
) -> str:
"""Atomic INSERT-with-cap. Returns one of:
- "added" — pin newly created
- "duplicate" — pin already existed for this tuple
- "at_cap" — tab already at max_pins, cannot add new pin

The cap check and INSERT happen in one SQL statement so two concurrent
calls cannot both pass an N=3 check and produce N=5. Tested at
concurrency to lock the invariant in.
"""
if not user_id:
raise ValueError("user_id is required")
if not profile_id:
raise ValueError("profile_id is required")
if not tab_id:
raise ValueError("tab_id is required")
if not agent_id:
raise ValueError("agent_id is required")
assert self._db is not None
pinned_at = datetime.now(timezone.utc).isoformat()
cursor = await self._db.execute(
"""
INSERT OR IGNORE INTO agent_pins
(user_id, profile_id, tab_id, agent_id, pinned_at)
SELECT ?, ?, ?, ?, ?
WHERE (
SELECT COUNT(*) FROM agent_pins
WHERE user_id = ? AND profile_id = ? AND tab_id = ?
) < ?
""",
(
user_id, profile_id, tab_id, agent_id, pinned_at,
user_id, profile_id, tab_id, max_pins,
),
)
await self._db.commit()
if cursor.rowcount > 0:
return "added"
# rowcount==0 — either duplicate (PK violation, swallowed by IGNORE)
# or at-cap (WHERE clause failed). Disambiguate with a single SELECT.
check = await self._db.execute(
"SELECT 1 FROM agent_pins "
"WHERE user_id = ? AND profile_id = ? AND tab_id = ? AND agent_id = ?",
(user_id, profile_id, tab_id, agent_id),
)
existing = await check.fetchone()
return "duplicate" if existing else "at_cap"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate max_pins before the insert.

max_pins <= 0 currently falls through to "at_cap"/"duplicate" instead of failing fast, which hides configuration bugs and can make a tab look permanently full. Add an upfront guard here so callers get a clear error for impossible caps.

Suggested fix
     async def add_pin_if_under_cap(
         self,
         *,
         user_id: str,
         profile_id: str,
         tab_id: str,
         agent_id: str,
         max_pins: int,
     ) -> str:
@@
         if not agent_id:
             raise ValueError("agent_id is required")
+        if max_pins < 1:
+            raise ValueError("max_pins must be >= 1")
         assert self._db is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/pins.py` around lines 36 -
90, In add_pin_if_under_cap, validate the max_pins argument up front and raise a
clear error for non-positive values (e.g., if max_pins <= 0: raise
ValueError("max_pins must be a positive integer")), so callers fail fast instead
of silently returning "at_cap"/"duplicate"; add this guard near the existing
user_id/profile_id/tab_id/agent_id checks before using max_pins in the INSERT
SELECT.

Comment on lines +92 to +139
async def list_pins_for_tab(
self,
*,
user_id: str,
profile_id: str,
tab_id: str,
) -> list[dict]:
"""Returns list of {agent_id, pinned_at} ordered by pinned_at ASC."""
assert self._db is not None
cursor = await self._db.execute(
"SELECT agent_id, pinned_at FROM agent_pins "
"WHERE user_id = ? AND profile_id = ? AND tab_id = ? "
"ORDER BY pinned_at ASC",
(user_id, profile_id, tab_id),
)
rows = await cursor.fetchall()
return [{"agent_id": r[0], "pinned_at": r[1]} for r in rows]

async def list_pins_for_user(self, *, user_id: str) -> list[dict]:
"""Returns list of {profile_id, tab_id, agent_id, pinned_at}."""
assert self._db is not None
cursor = await self._db.execute(
"SELECT profile_id, tab_id, agent_id, pinned_at FROM agent_pins "
"WHERE user_id = ? ORDER BY pinned_at ASC",
(user_id,),
)
rows = await cursor.fetchall()
return [
{"profile_id": r[0], "tab_id": r[1], "agent_id": r[2], "pinned_at": r[3]}
for r in rows
]

async def count_pins_for_tab(
self,
*,
user_id: str,
profile_id: str,
tab_id: str,
) -> int:
"""Returns the number of pins on the (user, profile, tab) tuple."""
assert self._db is not None
cursor = await self._db.execute(
"SELECT COUNT(*) FROM agent_pins "
"WHERE user_id = ? AND profile_id = ? AND tab_id = ?",
(user_id, profile_id, tab_id),
)
row = await cursor.fetchone()
return row[0] if row else 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the read-side API consistent with the write-side validation.

list_pins_for_tab, list_pins_for_user, and count_pins_for_tab skip the required-id checks that add_pin and delete_pin already enforce. Right now malformed calls degrade to empty results or 0, which masks upstream bugs and makes the pins API inconsistent.

Suggested fix
     async def list_pins_for_tab(
         self,
         *,
         user_id: str,
         profile_id: str,
         tab_id: str,
     ) -> list[dict]:
         """Returns list of {agent_id, pinned_at} ordered by pinned_at ASC."""
+        if not user_id:
+            raise ValueError("user_id is required")
+        if not profile_id:
+            raise ValueError("profile_id is required")
+        if not tab_id:
+            raise ValueError("tab_id is required")
         assert self._db is not None
@@
     async def list_pins_for_user(self, *, user_id: str) -> list[dict]:
         """Returns list of {profile_id, tab_id, agent_id, pinned_at}."""
+        if not user_id:
+            raise ValueError("user_id is required")
         assert self._db is not None
@@
     async def count_pins_for_tab(
         self,
         *,
         user_id: str,
         profile_id: str,
         tab_id: str,
     ) -> int:
         """Returns the number of pins on the (user, profile, tab) tuple."""
+        if not user_id:
+            raise ValueError("user_id is required")
+        if not profile_id:
+            raise ValueError("profile_id is required")
+        if not tab_id:
+            raise ValueError("tab_id is required")
         assert self._db is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/pins.py` around lines 92 -
139, The read methods skip the same required-id validation that
add_pin/delete_pin perform; update list_pins_for_tab and count_pins_for_tab to
call the same validation helpers for user/profile/tab (e.g.,
self._require_user_id(user_id), self._require_profile_id(profile_id),
self._require_tab_id(tab_id)) at the top of each method, and update
list_pins_for_user to call self._require_user_id(user_id) before DB access,
leaving the rest of the logic unchanged.

Comment on lines +113 to +122
async def set_push_mute(
self, user_id: str, agent_id: str, kind: str, muted: bool
) -> None:
"""Set or clear a per-(agent, kind) mute.

muted=True inserts (or replaces), muted=False deletes.
Raises ValueError if kind is not in _PUSH_MUTE_KINDS.
"""
if kind not in _PUSH_MUTE_KINDS:
raise ValueError(f"invalid kind: {kind}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing validation for user_id and agent_id.

Other methods in this mixin validate required string parameters before database operations (e.g., delete_push_subscription validates both IDs). This method only validates kind but not user_id or agent_id, which could lead to inserting rows with empty identifiers.

Proposed fix
     async def set_push_mute(
         self, user_id: str, agent_id: str, kind: str, muted: bool
     ) -> None:
         """Set or clear a per-(agent, kind) mute.

         muted=True inserts (or replaces), muted=False deletes.
         Raises ValueError if kind is not in _PUSH_MUTE_KINDS.
         """
+        if not user_id:
+            raise ValueError("user_id is required")
+        if not agent_id:
+            raise ValueError("agent_id is required")
         if kind not in _PUSH_MUTE_KINDS:
             raise ValueError(f"invalid kind: {kind}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/desktop_browser/store_mixins/push.py` around lines 113 -
122, In set_push_mute validate that user_id and agent_id are non-empty strings
(same style used in delete_push_subscription) before any DB operations: check
user_id and agent_id and raise ValueError with a clear message if either is
missing/empty, then proceed to validate kind against _PUSH_MUTE_KINDS and
perform the insert/delete; reference the set_push_mute method and mirror the
parameter validation used elsewhere in this mixin to prevent inserting rows with
empty identifiers.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Test comment

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Test

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

test

2 similar comments
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

test

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

test

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

WARNING

  • tinyagentos/routes/desktop_browser/store_mixins/history.py:38 - The search query is used to build a LIKE pattern with wildcards on both sides. This allows the user to use % and _ as wildcards in their search, which may be unintended if literal matching is expected. Consider escaping the user's input or documenting the behavior.
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py:40 - The host_pattern is not validated when setting a site permission. An invalid pattern (e.g., containing multiple asterisks) may lead to unexpected matching behavior. Consider validating that the pattern is either '', or starts with '.' followed by a string that does not contain any asterisks.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

WARNING

  • tinyagentos/routes/desktop_browser/store_mixins/history.py:38 - The search query is used to build a LIKE pattern with wildcards on both sides. This allows the user to use % and _ as wildcards in their search, which may be unintended if literal matching is expected. Consider escaping the user's input or documenting the behavior.
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py:40 - The host_pattern is not validated when setting a site permission. An invalid pattern (e.g., containing multiple asterisks) may lead to unexpected matching behavior. Consider validating that the pattern is either '', or starts with '.' followed by a string that does not contain any asterisks.

Files Reviewed (10 files)

  • tinyagentos/routes/desktop_browser/store_mixins/init.py
  • tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py
  • tinyagentos/routes/desktop_browser/store_mixins/capabilities.py
  • tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py
  • tinyagentos/routes/desktop_browser/store_mixins/history.py
  • tinyagentos/routes/desktop_browser/store_mixins/pins.py
  • tinyagentos/routes/desktop_browser/store_mixins/profiles.py
  • tinyagentos/routes/desktop_browser/store_mixins/push.py
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py
  • tinyagentos/routes/desktop_browser/store_mixins/windows.py

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Jun 3, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

WARNING

  • tinyagentos/routes/desktop_browser/store_mixins/history.py:38 - The search query is used to build a LIKE pattern with wildcards on both sides. This allows the user to use % and _ as wildcards in their search, which may be unintended if literal matching is expected. Consider escaping the user's input or documenting the behavior.
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py:40 - The host_pattern is not validated when setting a site permission. An invalid pattern (e.g., containing multiple asterisks) may lead to unexpected matching behavior. Consider validating that the pattern is either '', or starts with '.' followed by a string that does not contain any asterisks.

Files Reviewed (10 files)

  • tinyagentos/routes/desktop_browser/store_mixins/init.py
  • tinyagentos/routes/desktop_browser/store_mixins/bookmarks.py
  • tinyagentos/routes/desktop_browser/store_mixins/capabilities.py
  • tinyagentos/routes/desktop_browser/store_mixins/drive_sessions.py
  • tinyagentos/routes/desktop_browser/store_mixins/history.py
  • tinyagentos/routes/desktop_browser/store_mixins/pins.py
  • tinyagentos/routes/desktop_browser/store_mixins/profiles.py
  • tinyagentos/routes/desktop_browser/store_mixins/push.py
  • tinyagentos/routes/desktop_browser/store_mixins/site_permissions.py
  • tinyagentos/routes/desktop_browser/store_mixins/windows.py

Reviewed by nemotron-3-super-120b-a12b-20230311:free · 1,417,124 tokens

@jaylfc jaylfc merged commit 6a75be6 into dev Jun 3, 2026
7 checks passed
@jaylfc jaylfc deleted the refactor/browser-store-mixins branch June 3, 2026 20:11
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant