Skip to content

feat(auth): hoist auth dispatch into service layer (base + assistants)#401

Open
ibbybuilds wants to merge 6 commits into
mainfrom
feat/auth-dispatch-hoist
Open

feat(auth): hoist auth dispatch into service layer (base + assistants)#401
ibbybuilds wants to merge 6 commits into
mainfrom
feat/auth-dispatch-hoist

Conversation

@ibbybuilds

@ibbybuilds ibbybuilds commented May 31, 2026

Copy link
Copy Markdown
Member

First PR in the auth dispatch hoist workstream. Makes @auth.on dispatch mandatory at the service call site instead of optional route-handler boilerplate, so no endpoint can silently ship without authorization. Non-breaking; assistants only in this PR. Threads, runs, crons, and the breaking renames follow in separate PRs.

What changes

Authenticated service base (services/_authenticated.py)
Carries the request identity (session, user) and exposes one _dispatch(action, value) entry point. Tenant-scoped services inherit from it and call _dispatch at the top of each public method. Dispatch policy is unchanged: handle_event stays default-allow when no handler matches, and the SQL-layer user_id == user.identity filter remains the tenant boundary.

Auth filter builder (core/auth_filters.py)
Compiles an @auth.on handler's returned filter dict into a SQLAlchemy JSONB predicate over a metadata column:

  • {"owner": "u1"}metadata @> {"owner": "u1"}
  • {"owner": {"$eq": "u1"}} → same, explicit
  • {"tags": {"$contains": "x"}}metadata->'tags' @> ["x"]
  • {"$or": [...]} / {"$and": [...]} → OR / AND (nesting depth capped at 2)
  • multiple top-level keys → AND

Previously the service flattened every handler return into a single metadata @> containment, so $eq/$contains/$or/$and silently matched the wrong rows or none. Malformed filters now raise 500 with a clear message instead of failing silently.

Assistants migration
AssistantService inherits Authenticated; every method dispatches at entry. The per-method user_identity/user params are gone (identity is on the service), so routes shrink to parse-and-call. Two handler conventions are now cleanly separated:

  • create/update: handlers inject by mutating value["metadata"] in place; only that injection is read back onto the request.
  • search/read/count/versions: the handler return is a query filter applied as an AND'd WHERE clause, leaving the caller's own request.metadata untouched.

Also fixes the versions endpoint to dispatch assistants.search (not read) with the {assistant_id, metadata} value shape, per the auth dispatch spec — handlers scoping version listing were previously bypassed.

Tests

  • test_auth_filters.py — operator/boolean/envelope/validation coverage for the builder.
  • test_assistant_service.py::TestAuthDispatch — per-method dispatch tuples, 403-before-DB, filter-as-WHERE.
  • test_authenticated.py — base class dispatch + denial propagation.
  • Full suite: 1542 passing. Lint + type-check clean.

Follow-ups (separate PRs)

  • StoreService + store.searchstore.list_namespaces rename (breaking, batched for 0.10.0)
  • ThreadService extract + dispatch
  • RunService extract + missing-dispatch gaps + runs.*threads.* rename (breaking, batched for 0.10.0)
  • CronService multi-resource chain
  • Docs + migration guide + version bump

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Centralized assistant authorization and request handling to the service layer for more consistent behavior across assistant endpoints.
  • Bug Fixes

    • Improved metadata-filter evaluation with stricter validation and clearer error responses for malformed filters.
    • Consistent handling of graph/subgraph and version-related queries (query params normalized).
  • Tests

    • Expanded unit and integration coverage for auth dispatch and metadata filtering; updated tests to align with the new service-based auth flow.

Greptile Summary

This PR hoists @auth.on dispatch from route handlers into a new Authenticated service base, ensuring every DB-touching method authorizes before querying. It also introduces a proper JSONB filter compiler (auth_filters.py) that replaces the old flat-metadata-merge approach with full $eq/$contains/$or/$and support and explicit rejection of malformed filters.

  • Authenticated base + AssistantService refactor: _dispatch is now called at the top of every public method; _read_owned_assistant correctly funnels schemas/graph/subgraphs through a single auth-filtered lookup, closing the previously-reported bypass on those endpoints.
  • auth_filters.py: Compiles handler filter dicts into SQLAlchemy JSONB predicates with operator support, depth capping, and empty-branch rejection; previously-reported $or/$and TRUE-collapse issue is addressed.
  • update_assistant / set_assistant_latest re-read hazard: After committing the UPDATE, both methods re-execute the original stmt — which includes the handler's auth filter on metadata_dict. If the new metadata no longer satisfies the filter, session.scalar(stmt) returns None and to_pydantic(None) raises an unhandled AttributeError, producing a 500 after a committed write.

Confidence Score: 4/5

Safe to merge in environments without custom auth handlers returning metadata filters; the re-read bug only activates when a handler scopes by metadata and an update removes that metadata.

The auth dispatch consolidation is sound and the filter compiler is well-tested. The only functional defect introduced is in update_assistant and set_assistant_latest: both re-execute the pre-update SELECT (which carries the handler's metadata filter) after committing the write. If the committed metadata no longer matches the filter, the re-read returns None and to_pydantic(None) crashes — a 500 on a write that already landed in the database.

libs/aegra-api/src/aegra_api/services/assistant_service.py — specifically the post-commit scalar(stmt) calls in update_assistant (~line 446) and set_assistant_latest (~line 512).

Important Files Changed

Filename Overview
libs/aegra-api/src/aegra_api/services/assistant_service.py Hoists auth dispatch into the service; introduces _read_owned_assistant helper, but the post-commit re-read in update_assistant and set_assistant_latest reuses a stmt with the auth filter, causing a crash if updated metadata no longer satisfies the filter.
libs/aegra-api/src/aegra_api/core/auth_filters.py New JSONB filter compiler with correct operator handling, depth cap, empty-branch rejection, and metadata-envelope unwrapping; well-tested.
libs/aegra-api/src/aegra_api/services/authenticated.py New base class cleanly encapsulates session + user identity and exposes a single _dispatch entry point; straightforward and well-structured.
libs/aegra-api/src/aegra_api/api/assistants.py Route handlers correctly simplified to parse-and-delegate; auth boilerplate removed cleanly.
libs/aegra-api/tests/unit/test_services/test_assistant_service.py New TestAuthDispatch class thoroughly pins dispatch tuples, filter propagation, and 403-before-DB; contains inline imports that should be at file top per style guide.
libs/aegra-api/tests/unit/test_core/test_auth_filters.py Comprehensive coverage of all filter shapes, boolean ops, depth cap, empty-branch rejection, and envelope unwrapping.
libs/aegra-api/tests/unit/test_services/test_authenticated.py Concise base-class tests; imports correctly at file top.
libs/aegra-api/tests/integration/test_api/test_assistants_crud.py Correctly removes handle_event patch; old auth-layer regression tests removed with pointer to service-layer equivalents.
libs/aegra-api/tests/integration/test_services/test_assistant_service_db.py Mechanical update to remove user_identity params and monkeypatch handle_event for auth-filter path.

Sequence Diagram

sequenceDiagram
    participant Route as FastAPI Route
    participant Svc as AssistantService
    participant Auth as Authenticated._dispatch
    participant Handler as @auth.on handler
    participant DB as PostgreSQL

    Route->>Svc: create_assistant(request)
    Svc->>Auth: _dispatch("create", value)
    Auth->>Handler: handle_event(ctx, value)
    Handler-->>Auth: mutates value["metadata"] / raises 403
    Auth-->>Svc: None (return discarded for create)
    Svc->>DB: INSERT AssistantORM

    Route->>Svc: get_assistant(id)
    Svc->>Auth: "_dispatch("read", {assistant_id})"
    Auth->>Handler: handle_event(ctx, value)
    Handler-->>Auth: filter_dict or raises 403
    Auth-->>Svc: filter_dict
    Svc->>DB: "SELECT WHERE id=? AND user_id=? AND metadata @> filter"
    DB-->>Svc: AssistantORM or None (404)

    Route->>Svc: update_assistant(id, request)
    Svc->>Auth: _dispatch("update", value)
    Auth->>Handler: handle_event(ctx, value)
    Handler-->>Auth: filter_dict or raises 403
    Auth-->>Svc: filter_dict
    Svc->>DB: "SELECT WHERE id=? AND user_id=? AND metadata @> filter"
    DB-->>Svc: AssistantORM (or 404)
    Svc->>DB: "UPDATE SET metadata_dict=new_metadata"
    Svc->>DB: SELECT same stmt with old filter ⚠️
    DB-->>Svc: None if new metadata breaks filter → crash
Loading

Comments Outside Diff (1)

  1. libs/aegra-api/src/aegra_api/services/assistant_service.py, line 427-447 (link)

    P1 Stale auth filter on post-commit re-read

    update_assistant builds stmt with the auth filter before the UPDATE, then re-executes that same statement after committing new metadata. If request.metadata removes a field the handler filter requires (e.g. the auth filter is {"team": "x"} and the user clears metadata to {}), the post-commit session.scalar(stmt) returns None and to_pydantic(None) raises an AttributeError — a 500 after a write that already committed. The same problem exists in set_assistant_latest at line 512, where rolling back to an older version may swap in metadata that doesn't satisfy the current filter.

    Fix in Claude Code Fix in Cursor Fix in Codex

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Reviews (2): Last reviewed commit: "fix(auth): apply handler filters on all ..." | Re-trigger Greptile

ibbybuilds and others added 4 commits May 30, 2026 17:19
Introduces services/_authenticated.py with an Authenticated base that carries
the request identity (session, user) and exposes a single _dispatch(action,
value) entry point. Tenant-scoped services will inherit from this so auth
dispatch becomes mandatory at the service call site rather than scattered
across route handlers.

Dispatch policy is unchanged: handle_event stays default-allow when no handler
matches, and the SQL-layer user_id == user.identity filter remains the tenant
boundary (GHSA-m98r-6667-4wq7). This is purely structural groundwork for the
auth hoist; no route behavior changes yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AssistantService now inherits Authenticated and dispatches @auth.on.assistants.*
at the entry of each public method via self._dispatch(action, value). The
identity moves into the constructor (session, user, langgraph_service), so the
per-method user_identity/user parameters are gone and the routes drop all
build_auth_context/handle_event boilerplate — handlers shrink to parse-and-call.

Handler-returned filters now merge into the metadata containment predicate
inside the service via a single _merged_metadata helper (DRY), replacing the
three near-identical merge blocks that lived in the route layer. The #333
regression (a flat filter dict must not 500) and the read-endpoint dispatch
coverage (versions/schemas/graph/subgraphs fire assistants.read) move from
route-level tests to AssistantService unit tests where dispatch actually runs.

Dispatch policy is unchanged: handle_event stays default-allow when no handler
matches and the SQL-layer user_id == user.identity filter remains the tenant
boundary. The single patch chokepoint for tests is now
aegra_api.services._authenticated.handle_event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds core/auth_filters.py: compiles an @auth.on handler filter dict into a
SQLAlchemy JSONB predicate over a metadata column, per the documented filter
contract:

  {"owner": "u1"}                  -> metadata @> {"owner": "u1"}
  {"owner": {"$eq": "u1"}}         -> same, explicit
  {"tags": {"$contains": "x"}}     -> metadata->'tags' @> ["x"]
  {"$or": [...]} / {"$and": [...]} -> OR / AND (depth cap 2)
  multiple top-level keys          -> AND

Previously AssistantService flattened every handler return into a single
metadata @> containment, so $eq/$contains/$or/$and silently matched the wrong
rows (or none). Malformed filters now raise 500 with a clear message instead of
failing silently.

Separates the two handler conventions that were conflated:
- create/update: handlers inject by mutating value["metadata"] in place; only
  that injection is read back onto the request (the return dict is a query
  filter with no insert meaning).
- search/read/count/versions: the handler return is a query filter, applied as
  an AND'd WHERE clause via build_metadata_filter, leaving request.metadata
  (the caller's own filter) untouched.

Also fixes the versions endpoint to dispatch assistants.search (not read) with
the {assistant_id, metadata} value shape, per the auth dispatch spec — handlers
scoping version listing were previously bypassed.

The Aegra {"metadata": {...}} envelope handlers historically return
(examples/jwt_mock_auth_example.py) is unwrapped for back-compat. The SQL-layer
user_id == user.identity filter remains the tenant boundary; these filters are
additional optional narrowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_assistants_search_sort.py imported _merge_handler_filters_into_metadata,
removed from the route layer when filter merging moved into the service. Drops
the dead import and the obsolete TestMergeHandlerFilters class (its behavior is
now covered by test_auth_filters.py and test_assistant_service.py::TestAuthDispatch).
Keeps the _resolve_sort coverage. Also patches handle_event via the imported
module object so monkeypatch resolves the target reliably.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.33333% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ra-api/src/aegra_api/services/assistant_service.py 95.89% 3 Missing ⚠️
libs/aegra-api/src/aegra_api/core/auth_filters.py 98.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors authorization dispatch from FastAPI route handlers into the service layer. It introduces a new build_metadata_filter function to compile auth-handler filter dictionaries into SQLAlchemy JSONB predicates, a reusable Authenticated base class to manage _dispatch calls, and integrates both into AssistantService. All endpoint handlers now remove explicit user parameters and delegate entirely to the service.

Changes

Assistants Authorization Layer Refactoring

Layer / File(s) Summary
Auth Filter Compilation and Dispatch Base
libs/aegra-api/src/aegra_api/core/auth_filters.py, libs/aegra-api/src/aegra_api/services/authenticated.py
build_metadata_filter compiles auth handler filters into JSONB SQLAlchemy clauses, supporting $eq, $contains, $or, $and, and metadata-envelope unwrapping. Authenticated base class stores session and user, providing _dispatch(action, value) to invoke handlers and return filter objects for SQL composition.
AssistantService Auth Refactoring and Method Signature Updates
libs/aegra-api/src/aegra_api/services/assistant_service.py
AssistantService inherits from Authenticated, removes explicit user_identity/user parameters from all methods, uses self.user.identity consistently for query scoping, calls _dispatch at method entry to obtain auth-derived filters, applies build_metadata_filter to read/search queries, and injects handler-mutated metadata via _injected_metadata helper.
API Endpoint Simplification
libs/aegra-api/src/aegra_api/api/assistants.py
All endpoint handlers remove user: User = Depends(get_current_user) parameters, inline authorization/event handling, and metadata merging; handlers now only inject AssistantService and return service method results directly.
Auth Filter Compilation and Dispatch Unit Tests
libs/aegra-api/tests/unit/test_core/test_auth_filters.py, libs/aegra-api/tests/unit/test_services/test_authenticated.py
New test modules exercise build_metadata_filter for null/empty inputs, JSONB containment, $contains, logical operators, metadata unwrapping, and error handling; verify Authenticated._dispatch constructs context, returns handler filters, and propagates denial errors.
AssistantService Unit Tests
libs/aegra-api/tests/unit/test_services/test_assistant_service.py
Updated tests to new service signatures (no per-method user arguments); added TestAuthDispatch suite verifying dispatch resource/action mapping, rejection short-circuiting before DB calls, and SQL WHERE-parameter application without mutating request.metadata.
Integration Test Updates
libs/aegra-api/tests/integration/test_api/test_assistants_crud.py, libs/aegra-api/tests/integration/test_services/test_assistant_service_db.py, libs/aegra-api/tests/unit/test_api/test_assistants_search_sort.py
API integration tests remove auth-handler patching and use service dependency override; service integration tests construct AssistantService(session, User(...), langgraph_service) and call all methods without per-call user arguments; removed API-layer auth regression tests now in service unit suite; sorted-endpoint tests focus only on _resolve_sort behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • aegra/aegra#386: Both PRs touch assistant extra endpoints and authorization dispatch patterns; #386 adds endpoint-level dispatch helpers while this PR centralizes dispatch in services.
  • aegra/aegra#348: Related to /assistants/search changes; this PR moves filter-merge logic from route helpers into AssistantService + build_metadata_filter.
  • aegra/aegra#251: Related to passing authenticated User into LangGraphService; both PRs ensure graph/schema extraction receives correct user context.

Poem

🐰 I hopped through routes and shifted auth below,

Filters turned to SQL with a gentle glow.
Authenticated base, a warm burrowed nest,
Services now guard assistants with zest. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(auth): hoist auth dispatch into service layer (base + assistants)' directly and specifically summarizes the main change: moving authentication dispatch logic from route handlers into a new service base class and applying it to AssistantService.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/auth-dispatch-hoist

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.

The underscore-prefixed module name was inconsistent with the rest of
services/ (base classes use plain names like base_broker, base_executor). No
behavior change; pure rename of services/_authenticated.py -> authenticated.py
and its imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread libs/aegra-api/src/aegra_api/services/assistant_service.py Outdated
Comment thread libs/aegra-api/src/aegra_api/core/auth_filters.py
Comment thread libs/aegra-api/tests/unit/test_services/test_authenticated.py
Comment thread libs/aegra-api/src/aegra_api/services/authenticated.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
libs/aegra-api/src/aegra_api/services/assistant_service.py (5)

618-631: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same issue: read filters discarded.

Apply the filters returned by _dispatch("read", ...) to the query, consistent with get_assistant.

🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py` around lines 618
- 631, The query in get_assistant_subgraphs ignores the access-control filters
returned by await self._dispatch("read", {"assistant_id": assistant_id}); modify
get_assistant_subgraphs to capture the dispatch result (like in get_assistant),
extract and apply its returned filters/where-clause to the select(AssistantORM)
statement (or combine with existing where conditions) so the same access filters
from _dispatch("read", ...) are honored before calling self.session.scalar;
reference the get_assistant_subgraphs method and the _dispatch("read", ...) call
and follow the pattern used in get_assistant.

436-439: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against None after re-fetch.

After the UPDATE, session.scalar(stmt) may return None from the type checker's perspective. Add an assertion or raise to satisfy the type checker and catch any unexpected state.

🔧 Proposed fix
         await self.session.execute(assistant_update)
         await self.session.commit()
         updated_assistant = await self.session.scalar(stmt)
+        assert updated_assistant is not None, "Assistant disappeared after update"
         return to_pydantic(updated_assistant)
🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py` around lines 436
- 439, After executing the UPDATE and re-fetching with session.scalar(stmt) in
the assistant service method, guard against a None result by asserting or
raising if updated_assistant is None so the type checker and runtime are
satisfied; update the block that calls await
self.session.execute(assistant_update), await self.session.commit(), and
updated_assistant = await self.session.scalar(stmt) to check updated_assistant
(e.g., raise RuntimeError or AssertionError with a clear message including the
assistant id/context) before returning to_pydantic(updated_assistant).

530-543: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle potentially None version name.

v.name may be None (per pipeline error), but Assistant.name requires str. Provide a fallback.

🔧 Proposed fix
         version_list = [
             Assistant(
                 assistant_id=assistant_id,
-                name=v.name,
+                name=v.name or "",
                 description=v.description,
                 config=v.config or {},
                 context=v.context or {},
🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py` around lines 530
- 543, The Assistant construction in version_list can pass None for v.name which
violates Assistant.name's str requirement; update the Assistant(...) call (in
the version_list creation) to provide a fallback string when v.name is None
(e.g., use v.name or f"unnamed-{v.version}" or a fixed "<unnamed>" value) so
Assistant.name always receives a str; keep the rest of the fields unchanged and
ensure any chosen fallback is descriptive for debugging.

185-198: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix type errors: config may be None.

The pipeline failures indicate that request.config can be None, making config.get("configurable") and subscript operations fail type checking. Default to an empty dict.

🔧 Proposed fix
-        config = request.config
-        context = request.context
+        config = request.config or {}
+        context = request.context or {}
 
-        if config.get("configurable") and context:
+        if config.get("configurable") and context:
🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py` around lines 185
- 198, request.config can be None causing attribute/subscript errors; default
config to an empty dict and ensure any updates are written back to
request.config. Concretely, replace use of config = request.config with config =
request.config or {} (or equivalent null-coalescing), then keep the same checks
(config.get("configurable")) and assignments, and when you set
config["configurable"] = context make sure to assign the modified dict back to
request.config so the request object is updated.

496-499: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same None guard needed after re-fetch.

Same pattern as update_assistant — add assertion after re-fetching the updated row.

🔧 Proposed fix
         await self.session.execute(assistant_update)
         await self.session.commit()
         updated_assistant = await self.session.scalar(stmt)
+        assert updated_assistant is not None, "Assistant disappeared after update"
         return to_pydantic(updated_assistant)
🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py` around lines 496
- 499, After re-fetching the updated row using updated_assistant = await
self.session.scalar(stmt) add the same None-guard/assertion used in
update_assistant: verify updated_assistant is not None (raise an appropriate
exception or assert) before calling to_pydantic(updated_assistant). Locate the
re-fetch and return sequence (the self.session.scalar(stmt) call, the
updated_assistant variable, and the to_pydantic call) and insert the null-check
to avoid passing None into to_pydantic.
🧹 Nitpick comments (1)
libs/aegra-api/tests/unit/test_services/test_assistant_service.py (1)

1059-1063: ⚡ Quick win

_owned_assistant() appears unused.

This helper isn't referenced by any test in TestAuthDispatch (all dispatch tests short-circuit via scalar.return_value = None). Either drop it, or—if the intent was to also cover the ownership-pass-through path (handler allows, DB returns an owned row)—add that positive-path test so dispatch success isn't left untested.

🤖 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 `@libs/aegra-api/tests/unit/test_services/test_assistant_service.py` around
lines 1059 - 1063, The helper function _owned_assistant() is defined but unused;
either remove it or add a positive-path unit test in TestAuthDispatch to
exercise ownership-success flow: have the mocked query scalar.return_value
return _owned_assistant() (instead of None), invoke the dispatch handler (the
same call site used by other dispatch tests), and assert the handler
proceeds/returns the expected success result and that graph_id was accessed;
update TestAuthDispatch to include this case if you want ownership path covered,
otherwise delete the unused _owned_assistant() helper.
🤖 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 `@libs/aegra-api/src/aegra_api/services/assistant_service.py`:
- Around line 576-584: The read authorization filters produced by calling
self._dispatch("read", {"assistant_id": assistant_id}) are not applied to the
SQLAlchemy query in get_assistant_graph; modify get_assistant_graph to capture
the returned filters (as done in get_assistant) and incorporate them into the
select(AssistantORM) stmt (which currently filters only by
AssistantORM.assistant_id and user_id/system) so the final query uses those
dispatched read filters when calling self.session.scalar(stmt).
- Around line 549-556: The read dispatch in get_assistant_schemas discards the
returned filters so auth-scoped metadata filters are not applied; capture the
result of await self._dispatch("read", {"assistant_id": assistant_id}) into a
variable (as done in get_assistant) and apply those filters to the DB query
before executing (e.g., incorporate the returned filter into stmt or add stmt =
stmt.where(filters)); do the same fix pattern for get_assistant_graph and
get_assistant_subgraphs so authorization filtering is enforced consistently.

---

Outside diff comments:
In `@libs/aegra-api/src/aegra_api/services/assistant_service.py`:
- Around line 618-631: The query in get_assistant_subgraphs ignores the
access-control filters returned by await self._dispatch("read", {"assistant_id":
assistant_id}); modify get_assistant_subgraphs to capture the dispatch result
(like in get_assistant), extract and apply its returned filters/where-clause to
the select(AssistantORM) statement (or combine with existing where conditions)
so the same access filters from _dispatch("read", ...) are honored before
calling self.session.scalar; reference the get_assistant_subgraphs method and
the _dispatch("read", ...) call and follow the pattern used in get_assistant.
- Around line 436-439: After executing the UPDATE and re-fetching with
session.scalar(stmt) in the assistant service method, guard against a None
result by asserting or raising if updated_assistant is None so the type checker
and runtime are satisfied; update the block that calls await
self.session.execute(assistant_update), await self.session.commit(), and
updated_assistant = await self.session.scalar(stmt) to check updated_assistant
(e.g., raise RuntimeError or AssertionError with a clear message including the
assistant id/context) before returning to_pydantic(updated_assistant).
- Around line 530-543: The Assistant construction in version_list can pass None
for v.name which violates Assistant.name's str requirement; update the
Assistant(...) call (in the version_list creation) to provide a fallback string
when v.name is None (e.g., use v.name or f"unnamed-{v.version}" or a fixed
"<unnamed>" value) so Assistant.name always receives a str; keep the rest of the
fields unchanged and ensure any chosen fallback is descriptive for debugging.
- Around line 185-198: request.config can be None causing attribute/subscript
errors; default config to an empty dict and ensure any updates are written back
to request.config. Concretely, replace use of config = request.config with
config = request.config or {} (or equivalent null-coalescing), then keep the
same checks (config.get("configurable")) and assignments, and when you set
config["configurable"] = context make sure to assign the modified dict back to
request.config so the request object is updated.
- Around line 496-499: After re-fetching the updated row using updated_assistant
= await self.session.scalar(stmt) add the same None-guard/assertion used in
update_assistant: verify updated_assistant is not None (raise an appropriate
exception or assert) before calling to_pydantic(updated_assistant). Locate the
re-fetch and return sequence (the self.session.scalar(stmt) call, the
updated_assistant variable, and the to_pydantic call) and insert the null-check
to avoid passing None into to_pydantic.

---

Nitpick comments:
In `@libs/aegra-api/tests/unit/test_services/test_assistant_service.py`:
- Around line 1059-1063: The helper function _owned_assistant() is defined but
unused; either remove it or add a positive-path unit test in TestAuthDispatch to
exercise ownership-success flow: have the mocked query scalar.return_value
return _owned_assistant() (instead of None), invoke the dispatch handler (the
same call site used by other dispatch tests), and assert the handler
proceeds/returns the expected success result and that graph_id was accessed;
update TestAuthDispatch to include this case if you want ownership path covered,
otherwise delete the unused _owned_assistant() helper.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f0ce72d-a91f-44d0-90ab-05f06e8ae797

📥 Commits

Reviewing files that changed from the base of the PR and between c5b8d74 and ea79b1a.

📒 Files selected for processing (10)
  • libs/aegra-api/src/aegra_api/api/assistants.py
  • libs/aegra-api/src/aegra_api/core/auth_filters.py
  • libs/aegra-api/src/aegra_api/services/assistant_service.py
  • libs/aegra-api/src/aegra_api/services/authenticated.py
  • libs/aegra-api/tests/integration/test_api/test_assistants_crud.py
  • libs/aegra-api/tests/integration/test_services/test_assistant_service_db.py
  • libs/aegra-api/tests/unit/test_api/test_assistants_search_sort.py
  • libs/aegra-api/tests/unit/test_core/test_auth_filters.py
  • libs/aegra-api/tests/unit/test_services/test_assistant_service.py
  • libs/aegra-api/tests/unit/test_services/test_authenticated.py

Comment thread libs/aegra-api/src/aegra_api/services/assistant_service.py Outdated
Comment thread libs/aegra-api/src/aegra_api/services/assistant_service.py Outdated
Review fixes for PR #401.

- get_assistant_schemas/graph/subgraphs, update_assistant, delete_assistant,
  and set_assistant_latest dispatched their action but discarded the returned
  filter, so a metadata-scoping handler was bypassable via those endpoints
  (the SQL user_id boundary still held). They now capture and apply the filter
  to the lookup query, matching get_assistant. The three read-derived endpoints
  share a _read_owned_assistant helper that dispatches, applies the filter, and
  404s — removing the duplicated lookup block.

- build_metadata_filter rejects empty or non-dict $or/$and branches. An empty
  branch compiled to and_() == TRUE, collapsing OR to TRUE and silently
  bypassing the constraint.

- Hoist an inline HTTPException import to the top of test_authenticated.py.

- Trim history-narrating docstrings on authenticated.py and _injected_metadata
  down to rationale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant