feat: implement network hosting and multi-user access#1032
Conversation
WalkthroughAdds server-side JWT sessions using per-token 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
There was a problem hiding this comment.
Code Review
This pull request implements a robust session management system featuring a hybrid in-memory/SQLite store, JWT revocation via jti claims, and dedicated management endpoints. It also introduces optimistic concurrency control for settings using ETags, support for TLS and trusted proxy configurations, and user-scoped WebSocket channels with presence tracking. Review feedback identifies critical Python 3 syntax errors in exception handling across multiple files, a potential race condition in the 'check-then-act' concurrency pattern for settings, and unused methods within the session store that should be integrated or removed.
| try: | ||
| if app_state._session_store is not None: # noqa: SLF001 | ||
| await app_state._session_store.cleanup_expired() # noqa: SLF001 | ||
| except MemoryError, RecursionError: |
There was a problem hiding this comment.
Invalid exception handling syntax. In Python 3, multiple exceptions must be grouped in a tuple using parentheses. The current syntax except E1, E2: is interpreted as except E1 as E2:, which will fail to catch RecursionError and instead assign the MemoryError instance to a variable named RecursionError.
except (MemoryError, RecursionError):| session_id=session_id, | ||
| user_id=user.id, | ||
| ) | ||
| except MemoryError, RecursionError: |
| async def _check_setting_etag( | ||
| request: Request[Any, Any, Any], | ||
| app_state: AppState, | ||
| namespace: str, | ||
| key: str, | ||
| ) -> None: | ||
| """Validate If-Match header against current setting ETag.""" | ||
| if_match = request.headers.get("if-match") | ||
| if not if_match: | ||
| return | ||
| try: | ||
| current = await app_state.settings_service.get_entry( | ||
| namespace, | ||
| key, | ||
| ) | ||
| except SettingNotFoundError as exc: | ||
| raise NotFoundException(str(exc)) from exc | ||
| current_etag = compute_etag( | ||
| current.value, | ||
| current.updated_at or "", | ||
| ) | ||
| check_if_match(if_match, current_etag, f"{namespace}:{key}") |
There was a problem hiding this comment.
The optimistic concurrency implementation uses a 'check-then-act' pattern in the controller which is susceptible to race conditions in a multi-user environment. Between the ETag validation in _check_setting_etag and the actual DB update in settings_service.set, another request could modify the setting. To ensure true optimistic concurrency, the version check (ETag or timestamp) should be part of an atomic database operation (e.g., UPDATE ... WHERE updated_at = ?).
| async def update_last_active( | ||
| self, | ||
| session_id: str, | ||
| timestamp: datetime, | ||
| ) -> None: | ||
| """Update the last-active timestamp for a session. | ||
|
|
||
| Args: | ||
| session_id: The JWT ``jti`` claim. | ||
| timestamp: The new last-active timestamp. | ||
| """ | ||
| await self._db.execute( | ||
| "UPDATE sessions SET last_active_at = ? WHERE session_id = ?", | ||
| (timestamp.isoformat(), session_id), | ||
| ) | ||
| await self._db.commit() |
There was a problem hiding this comment.
The update_last_active method is implemented but never called within the current PR changes (middleware, controllers, or WebSocket handlers). This results in the last_active_at field remaining static after session creation, which contradicts the PR description's mention of 'Last request timestamp (debounced)'. Please wire this into the authentication middleware or remove the dead code.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1032 +/- ##
==========================================
- Coverage 91.73% 91.45% -0.29%
==========================================
Files 682 686 +4
Lines 37912 38324 +412
Branches 3768 3818 +50
==========================================
+ Hits 34780 35050 +270
- Misses 2484 2613 +129
- Partials 648 661 +13 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR transitions SynthOrg’s API from local-first toward network-ready, multi-user operation by adding TLS/proxy configuration, durable JWT session tracking/revocation, optimistic concurrency for settings, and per-user WebSocket isolation/presence tracking.
Changes:
- Add server TLS + trusted-proxy configuration and pass the relevant kwargs through to Uvicorn.
- Implement JWT session persistence/revocation with background cleanup and new session-management auth endpoints.
- Add ETag/If-Match optimistic concurrency utilities and wire ETag handling into settings endpoints; extend WS channels with per-user isolation + presence.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/persistence/sqlite/test_migrations.py | Updates schema migration expectations for new sessions table + indexes. |
| tests/unit/api/test_server.py | Adds unit coverage for TLS + proxy-related uvicorn.run() kwargs. |
| tests/unit/api/test_errors.py | Extends expected error-code enum coverage for new codes. |
| tests/unit/api/test_config.py | Adds validation tests for TLS pairing + trusted proxies in ServerConfig. |
| tests/unit/api/test_concurrency.py | New tests for ETag computation and If-Match conflict behavior. |
| tests/unit/api/test_channels.py | Updates channel plugin expectation for dynamic channels support. |
| tests/unit/api/controllers/test_ws.py | Updates WS controller tests for new message handler signature/user context. |
| tests/unit/api/conftest.py | Adapts token fixture usage to create_token()’s new return shape. |
| tests/unit/api/auth/test_session_store.py | New tests for session persistence, listing, revocation, and cleanup. |
| tests/unit/api/auth/test_service.py | Updates JWT tests to validate jti/session-id behavior. |
| tests/unit/api/auth/test_presence.py | New tests for in-memory WebSocket presence tracking. |
| tests/unit/api/auth/test_middleware.py | Updates middleware tests for create_token() return shape. |
| src/synthorg/settings/definitions/api.py | Registers new restart-required API settings for TLS + trusted proxies. |
| src/synthorg/persistence/sqlite/schema.sql | Adds sessions table + indexes to SQLite schema. |
| src/synthorg/observability/events/api.py | Adds new observability event constants for sessions/TLS/concurrency/WS isolation. |
| src/synthorg/api/state.py | Adds SessionStore and UserPresence to AppState with accessors/wiring. |
| src/synthorg/api/server.py | Passes TLS + trusted-proxy kwargs to Uvicorn and logs TLS configuration. |
| src/synthorg/api/lifecycle.py | Initializes SessionStore at startup and loads revoked session IDs. |
| src/synthorg/api/errors.py | Introduces VersionConflictError and adds new error codes to the enum. |
| src/synthorg/api/controllers/ws.py | Implements per-user channels, server-side channel authorization, and presence connect/disconnect tracking. |
| src/synthorg/api/controllers/settings.py | Adds ETag responses and If-Match enforcement for optimistic concurrency on settings updates. |
| src/synthorg/api/config.py | Adds TLS/proxy fields to ServerConfig with pairwise validation and exposure warnings. |
| src/synthorg/api/concurrency.py | New ETag and If-Match utilities + conflict logging. |
| src/synthorg/api/channels.py | Adds user:{id} channel helpers and enables arbitrary channels for dynamic user channels. |
| src/synthorg/api/auth/session.py | New session domain model corresponding to JWT jti. |
| src/synthorg/api/auth/session_store.py | New hybrid in-memory + SQLite session store with revocation persistence. |
| src/synthorg/api/auth/service.py | Adds jti to JWTs and returns session id from create_token(). |
| src/synthorg/api/auth/presence.py | New in-memory presence tracker for WS connections (multi-tab aware). |
| src/synthorg/api/auth/middleware.py | Adds O(1) revocation checks in JWT auth hot path. |
| src/synthorg/api/auth/controller.py | Adds session creation on login/setup and session-management endpoints (list/revoke/logout). |
| src/synthorg/api/auth/init.py | Exports Session and SessionStore from the auth package. |
| src/synthorg/api/app.py | Extends periodic cleanup loop to prune expired sessions. |
| docs/errors.md | Documents new error codes. |
| docs/design/page-structure.md | Updates auth page endpoint list to include session management and logout. |
| docs/design/operations.md | Updates API endpoint overview to mention session management/logout. |
| CLAUDE.md | Updates repo/module overview to include sessions, concurrency, TLS config, and presence. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @model_validator(mode="after") | ||
| def _validate_tls_pair(self) -> Self: | ||
| """Require both cert and key when either is set.""" | ||
| has_cert = self.ssl_certfile is not None | ||
| has_key = self.ssl_keyfile is not None | ||
| has_ca = self.ssl_ca_certs is not None | ||
|
|
||
| if has_cert and not has_key: | ||
| msg = "ssl_keyfile is required when ssl_certfile is set" | ||
| raise ValueError(msg) | ||
| if has_key and not has_cert: | ||
| msg = "ssl_certfile is required when ssl_keyfile is set" | ||
| raise ValueError(msg) | ||
| if has_ca and not has_cert: | ||
| msg = "ssl_certfile is required when ssl_ca_certs is set" | ||
| raise ValueError(msg) | ||
|
|
||
| if ( | ||
| self.host == "0.0.0.0" # noqa: S104 | ||
| and not has_cert | ||
| and not self.trusted_proxies | ||
| ): | ||
| logger.warning( | ||
| API_NETWORK_EXPOSURE_WARNING, | ||
| host=self.host, | ||
| note=( | ||
| "Server binds to all interfaces without TLS " |
There was a problem hiding this comment.
TLS presence checks treat empty strings as “configured” because they use is not None (has_cert/has_key/has_ca). If config values come through as "" (e.g., via settings defaults / YAML), validation will (a) suppress the 0.0.0.0 exposure warning and (b) consider TLS “set”, while run_server() won’t pass TLS kwargs because it checks truthiness (if server.ssl_certfile:). Consider normalizing "" -> None (e.g., in a mode="before" validator) and/or changing has_* to use bool(self.ssl_certfile)/strip() so validation and runtime behavior match.
| # Check session revocation (sync, O(1) in-memory lookup). | ||
| jti = claims.get("jti") | ||
| if jti and hasattr(app_state, "_session_store"): | ||
| session_store = app_state._session_store # noqa: SLF001 | ||
| if session_store is not None and session_store.is_revoked(jti): | ||
| logger.warning( | ||
| API_AUTH_FAILED, | ||
| reason="session_revoked", | ||
| jti=jti, | ||
| path=path, | ||
| ) | ||
| return None | ||
|
|
There was a problem hiding this comment.
The revocation path currently returns None on a revoked jti, which later becomes a generic 401 UNAUTHORIZED via NotAuthorizedException. This doesn’t appear to ever surface the newly added ErrorCode.SESSION_REVOKED (1002) described in the PR. If the intent is to return a distinct machine-readable error code for revoked sessions, raise a dedicated ApiError (e.g., SessionRevokedError mapping to 401 + SESSION_REVOKED) or otherwise route this condition through the API error handler instead of treating it as an invalid JWT.
| # Session store shares the persistence db connection. | ||
| try: | ||
| db = persistence.users._db # type: ignore[attr-defined] # noqa: SLF001 | ||
| session_store = SessionStore(db) | ||
| await session_store.load_revoked() | ||
| app_state.set_session_store(session_store) | ||
| logger.info( |
There was a problem hiding this comment.
Session store initialization relies on reaching into persistence.users._db (a private attribute of the SQLite user repository). This tightly couples lifecycle wiring to a specific persistence implementation and breaks encapsulation. Prefer exposing the shared aiosqlite.Connection via an explicit persistence-backend API (e.g., SQLitePersistenceBackend.db / get_connection()), or have the persistence layer construct and own SessionStore so callers don’t depend on repository internals.
src/synthorg/api/concurrency.py
Outdated
| """Compute a weak ETag from value and timestamp. | ||
|
|
||
| Uses SHA-256 truncated to 16 hex characters, prefixed with | ||
| ``W/`` per RFC 7232 (weak validator -- the representation | ||
| may vary by encoding). | ||
|
|
||
| Args: | ||
| value: Resource value (e.g. setting value, config JSON). | ||
| updated_at: Last-modified timestamp string. | ||
|
|
||
| Returns: | ||
| Weak ETag string like ``W/"a1b2c3d4e5f6g7h8"``. | ||
| """ | ||
| digest = hashlib.sha256( | ||
| f"{value}:{updated_at}".encode(), | ||
| ).hexdigest()[:16] | ||
| return f'W/"{digest}"' |
There was a problem hiding this comment.
The compute_etag() docstring example shows W/"a1b2c3d4e5f6g7h8", but SHA-256 hex digests only contain [0-9a-f] (no g/h). Update the example to a valid hex string to avoid misleading documentation.
| @property | ||
| def session_store(self) -> SessionStore: | ||
| """Return the JWT session store.""" | ||
| return self._require_service( | ||
| self._session_store, | ||
| "session_store", | ||
| ) | ||
|
|
||
| def set_session_store(self, store: SessionStore) -> None: | ||
| """Set the session store (deferred initialisation). | ||
|
|
||
| Args: | ||
| store: Configured session store. | ||
| """ | ||
| self._session_store = store | ||
|
|
There was a problem hiding this comment.
Call sites (e.g., auth middleware + cleanup loop) access _session_store directly and need # noqa: SLF001. Since SessionStore is optional/deferred like other AppState services, consider adding a has_session_store boolean property (consistent with has_persistence, has_task_engine, etc.) and updating callers to use app_state.session_store guarded by has_session_store. This reduces private attribute access and makes the API surface more consistent.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/synthorg/settings/definitions/api.py (1)
1-7:⚠️ Potential issue | 🟡 MinorStale docstring: setting count is outdated.
The module docstring states "11 settings" but this file now registers 15 settings after adding the 4 new TLS/proxy entries.
Suggested fix
-"""API namespace setting definitions. - -Registers 11 settings covering server, CORS, rate limiting, -authentication, and setup. Five are runtime-editable; six are -bootstrap-only (``restart_required=True``) because Litestar bakes -middleware and CORS into the application at construction time. -""" +"""API namespace setting definitions. + +Registers 15 settings covering server, TLS, CORS, rate limiting, +authentication, and setup. Five are runtime-editable; ten are +bootstrap-only (``restart_required=True``) because Litestar bakes +middleware and CORS into the application at construction time. +"""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/settings/definitions/api.py` around lines 1 - 7, The module docstring in src/synthorg/settings/definitions/api.py incorrectly states "11 settings"; update that top-of-file docstring to reflect the current number of settings (15) and any updated grouping if present so it matches the actual registrations in this module (e.g., the server, CORS, rate limiting, authentication, setup entries plus the four new TLS/proxy settings); edit the triple-quoted string at the top of the file to replace "11" with "15" and adjust any related phrasing (such as counts of runtime-editable vs bootstrap-only settings) to match the actual definitions registered by this module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/auth/controller.py`:
- Around line 784-795: Silent success occurs when _extract_jti(request) returns
falsy; add a DEBUG-level log to record this path for troubleshooting. In the
block where you currently return if not jti, call logger.debug with a clear
message including relevant identifiers (e.g., auth_user.user_id and any
request/session metadata available) so missing jti events are recorded; leave
the revoke and logger.info(API_SESSION_FORCE_LOGOUT, ...) behavior unchanged
when jti exists. Ensure the new log uses the same logger instance and does not
change control flow.
- Around line 286-295: The except Exception handler currently logs a failure
using the API_SESSION_CREATED event constant which is misleading; update the
logger call in the exception block (the except Exception clause in the session
creation flow) to use a failure-specific event constant (e.g.,
API_SESSION_CREATION_FAILED) or at minimum change the event param to a clear
failure string and adjust the log level if appropriate, keeping the rest of the
context (session_id, user_id, exc_info=True) intact so log consumers can
distinguish success vs failure.
- Around line 304-318: Change the broad exception caught when decoding tokens to
the specific jwt.InvalidTokenError: in the block that calls
request.app.state["app_state"] and app_state.auth_service.decode_token(token)
replace the except jwt.PyJWTError: handler with except jwt.InvalidTokenError: so
the helper mirrors the middleware’s expected exception type and only handles
token decode failures explicitly; keep the generic Exception handler and logging
for other failures unchanged.
- Around line 691-707: Validate the incoming scope parameter in the sessions
listing handler: ensure scope is one of the allowed values ("own" or "all") and
return a 400/BadRequest with a clear message if not; then keep the existing
logic that calls store.list_all() when scope == "all" and auth_user.role ==
HumanRole.CEO, otherwise call store.list_by_user(auth_user.user_id). Add the
check near the start of the function (before using scope) and consider replacing
the raw string with an enum or constant for allowed scopes to make comparisons
clearer; reference the AuthenticatedUser type, HumanRole.CEO, and the
store.list_all / store.list_by_user calls when locating where to add the
validation.
In `@src/synthorg/api/auth/presence.py`:
- Around line 8-10: The file currently creates a logger (logger =
get_logger(__name__)) that’s unused; either remove the import/instantiation or
add DEBUG logs inside the presence state handlers — update the logger usage in
the connect and disconnect functions (or similarly named methods handling
presence) to call logger.debug with contextual messages (e.g., "presence.connect
user_id=..." and "presence.disconnect user_id=...") so the logger variable is
used, otherwise delete the get_logger import and the logger =
get_logger(__name__) line to avoid an unused import.
In `@src/synthorg/api/auth/session_store.py`:
- Around line 256-271: The update_last_active method currently performs an
UPDATE that quietly does nothing if the session_id is missing; modify
update_last_active to inspect the statement result (e.g., use the returned
cursor/statement object from await self._db.execute in update_last_active) and,
if no rows were affected (rowcount == 0), emit a DEBUG log via
self._logger.debug with context including the session_id and timestamp;
otherwise proceed to commit as before. Ensure you reference update_last_active
and self._db.execute and that self._logger exists (create or use an existing
logger on the class if necessary).
In `@src/synthorg/api/concurrency.py`:
- Around line 19-37: compute_etag currently returns a weak ETag (prefixing with
W/) but check_if_match uses it with If-Match which per RFC7232 requires strong
validators; change compute_etag to emit a strong ETag (remove the W/ prefix,
keep the quoted hex string) so If-Match uses a strong validator, and verify
check_if_match still expects the quoted digest from compute_etag; alternatively,
if weak semantics were intended, update check_if_match to use If-None-Match
logic—choose the strong-ETag route and remove the W/ in compute_etag to fix the
RFC violation.
In `@src/synthorg/api/controllers/settings.py`:
- Around line 162-184: The current ETag check in _check_setting_etag is
TOCTOU-prone—make settings_service.set accept an optional expected_etag or
expected_updated_at and propagate that to repository.set so the DB performs an
atomic check-and-update (UPDATE ... WHERE namespace=? AND key=? AND updated_at=?
or equivalent) and returns a success flag; update settings_service.set and
repository.set to raise/return a ConflictException when the expected_updated_at
doesn't match (or return False and have controller translate to 409), and keep
_check_setting_etag only as an early advisory validation (or remove its
enforcement) so the real concurrency guarantee is enforced in
settings_service.set/repository.set.
In `@src/synthorg/api/errors.py`:
- Line 43: SESSION_REVOKED (1002) is defined but never returned to clients;
update the authentication failure path to emit this specific error code when a
token is revoked. Locate the token verification / auth middleware (e.g.,
functions named verify_token, authenticate_request, or the auth exception
handler) and add a branch that detects revoked tokens and raises/returns an API
error that includes SESSION_REVOKED (1002) as the error code instead of the
generic unauthorized response; ensure the emitted error uses the same error
response schema used elsewhere so clients can distinguish revoked sessions.
In `@src/synthorg/api/lifecycle.py`:
- Line 216: The code currently reaches into repository internals by calling
persistence.users._db; instead add a public accessor on the persistence layer
(e.g., persistence.get_db() or persistence.users.get_db()) that returns the
DB/connection and use that accessor in lifecycle.py instead of _db. Implement
the new accessor in the persistence module (exposing the concrete DB object from
the users repository), update the call site where db = persistence.users._db to
db = persistence.get_db() or db = persistence.users.get_db(), and ensure type
hints and any existing callers are updated to use the public method rather than
the _db attribute.
- Around line 224-229: The current except Exception block around session store
init (logged with API_APP_STARTUP) treats failures as non-fatal, which permits
startup without revocation support; change it to treat initialization failure as
fatal: log the error (include exc_info/exception details) at error level and
either re-raise the exception or terminate startup (e.g., sys.exit(1)) so the
app does not start without a functioning session store used by JWT jti
revocation checks; update the except block that wraps the session store init and
replace logger.warning with logger.error and a fatal fail/raise to prevent
fail-open behavior.
In `@src/synthorg/api/state.py`:
- Around line 451-458: The setter set_session_store currently overwrites
self._session_store silently; change it to behave like
set_auth_service/set_task_engine by guarding against reassignment (only allow
setting when self._session_store is None and raise/return if already set) and
add an INFO-level state-transition log when the store is assigned; reference the
set_session_store method and the _session_store attribute and mirror the
guard+processLogger.info call pattern used in set_auth_service and
set_task_engine.
---
Outside diff comments:
In `@src/synthorg/settings/definitions/api.py`:
- Around line 1-7: The module docstring in
src/synthorg/settings/definitions/api.py incorrectly states "11 settings";
update that top-of-file docstring to reflect the current number of settings (15)
and any updated grouping if present so it matches the actual registrations in
this module (e.g., the server, CORS, rate limiting, authentication, setup
entries plus the four new TLS/proxy settings); edit the triple-quoted string at
the top of the file to replace "11" with "15" and adjust any related phrasing
(such as counts of runtime-editable vs bootstrap-only settings) to match the
actual definitions registered by this module.
🪄 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: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ebdb1d54-0747-452d-ac51-e531bc5be7b1
📒 Files selected for processing (36)
CLAUDE.mddocs/design/operations.mddocs/design/page-structure.mddocs/errors.mdsrc/synthorg/api/app.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/auth/controller.pysrc/synthorg/api/auth/middleware.pysrc/synthorg/api/auth/presence.pysrc/synthorg/api/auth/service.pysrc/synthorg/api/auth/session.pysrc/synthorg/api/auth/session_store.pysrc/synthorg/api/channels.pysrc/synthorg/api/concurrency.pysrc/synthorg/api/config.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/errors.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/server.pysrc/synthorg/api/state.pysrc/synthorg/observability/events/api.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/settings/definitions/api.pytests/unit/api/auth/test_middleware.pytests/unit/api/auth/test_presence.pytests/unit/api/auth/test_service.pytests/unit/api/auth/test_session_store.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_channels.pytests/unit/api/test_concurrency.pytests/unit/api/test_config.pytests/unit/api/test_errors.pytests/unit/api/test_server.pytests/unit/persistence/sqlite/test_migrations.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Agent
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Nofrom __future__ import annotationsin Python code -- Python 3.14 has PEP 649
Use PEP 758 except syntax:except A, B:(no parentheses) -- ruff enforces this on Python 3.14
All public functions must have type hints and pass mypy strict mode
Docstrings must use Google style and are required on all public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries,BaseTool), usecopy.deepcopy()at construction +MappingProxyTypewrapping for read-only enforcement.
Fordict/listfields in frozen Pydantic models, rely onfrozen=Truefor field reassignment prevention andcopy.deepcopy()at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (usingmodel_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel,model_validator,computed_field,ConfigDict). Always useallow_inf_nan=FalseinConfigDictdeclarations to rejectNaN/Infin numeric fields at validation time.
Use@computed_fieldfor derived values instead of storing + validating redundant fields (e.g.TokenUsage.total_tokens)
UseNotBlankStr(fromcore.types) for all identifier/name fields -- including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants -- instead of manual whitespace validators.
Preferasyncio.TaskGroupfor fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over barecreate_task.
Maximum line length: 88 characters (enforced by ruff)
Functions must be less than 50 lines, files must be less than 800 lines
Files:
tests/unit/api/test_channels.pytests/unit/api/conftest.pytests/unit/persistence/sqlite/test_migrations.pysrc/synthorg/api/auth/middleware.pytests/unit/api/auth/test_service.pytests/unit/api/auth/test_middleware.pysrc/synthorg/api/server.pysrc/synthorg/api/auth/__init__.pytests/unit/api/test_concurrency.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/app.pytests/unit/api/auth/test_presence.pysrc/synthorg/api/auth/service.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_config.pytests/unit/api/test_server.pysrc/synthorg/api/concurrency.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/auth/session.pysrc/synthorg/api/errors.pysrc/synthorg/api/config.pytests/unit/api/auth/test_session_store.pytests/unit/api/test_errors.pysrc/synthorg/api/channels.pysrc/synthorg/observability/events/api.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.pysrc/synthorg/api/auth/presence.pysrc/synthorg/api/auth/session_store.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: All test files must use markers:@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.e2e,@pytest.mark.slow
Async tests: useasyncio_mode = "auto"-- no manual@pytest.mark.asyncioneeded
Test timeout: 30 seconds per test (global inpyproject.toml-- do not add per-filepytest.mark.timeout(30)markers; non-default overrides liketimeout(60)are allowed)
Prefer@pytest.mark.parametrizefor testing similar cases
For property-based testing in Python, use Hypothesis (@given+@settings). Profiles:ci(50 examples, default) anddev(1000 examples). Run dev profile:HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n 8 -k properties
Never skip, dismiss, or ignore flaky tests -- always fix them fully and fundamentally. For timing-sensitive tests, mocktime.monotonic()andasyncio.sleep(). For tasks that must block indefinitely, useasyncio.Event().wait()instead ofasyncio.sleep(large_number).
Files:
tests/unit/api/test_channels.pytests/unit/api/conftest.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/api/auth/test_service.pytests/unit/api/auth/test_middleware.pytests/unit/api/test_concurrency.pytests/unit/api/auth/test_presence.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_config.pytests/unit/api/test_server.pytests/unit/api/auth/test_session_store.pytests/unit/api/test_errors.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging/logging.getLogger()/print()in application code (exceptions:observability/setup.py,observability/sinks.py,observability/syslog_handler.py, andobservability/http_handler.pymay use stdlibloggingandprint(..., file=sys.stderr))
Logger variable name must always belogger(not_logger, notlog)
Always use event constants from domain-specific modules undersynthorg.observability.events(e.g.,API_REQUEST_STARTEDfromevents.api). Import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs for logging:logger.info(EVENT, key=value)-- neverlogger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG level for object creation, internal flow, entry/exit of key functions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names:example-provider,example-large-001,example-medium-001,example-small-001,large/medium/small.
In Pydantic models, useallow_inf_nan=Falsein allConfigDictdeclarations
Pure data models, enums, and re-exports do NOT need logging
Files:
src/synthorg/api/auth/middleware.pysrc/synthorg/api/server.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/app.pysrc/synthorg/api/auth/service.pysrc/synthorg/api/concurrency.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/auth/session.pysrc/synthorg/api/errors.pysrc/synthorg/api/config.pysrc/synthorg/api/channels.pysrc/synthorg/observability/events/api.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.pysrc/synthorg/api/auth/presence.pysrc/synthorg/api/auth/session_store.py
🧠 Learnings (40)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
CLAUDE.mdsrc/synthorg/api/auth/__init__.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/app.pytests/unit/api/controllers/test_ws.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/channels.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
CLAUDE.mdsrc/synthorg/api/auth/__init__.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: CLI (Go binary): see `cli/CLAUDE.md` for commands, flags, and reference; use `go -C cli` (never `cd cli`)
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Applied to files:
CLAUDE.mdsrc/synthorg/api/auth/__init__.pytests/unit/api/controllers/test_ws.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
CLAUDE.mdsrc/synthorg/api/app.pytests/unit/api/controllers/test_ws.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/errors.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
CLAUDE.mdsrc/synthorg/api/auth/__init__.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Applied to files:
CLAUDE.mdsrc/synthorg/api/auth/middleware.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/app.pytests/unit/api/controllers/test_ws.pysrc/synthorg/api/state.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to src/synthorg/providers/**/*.py : Providers: LLM provider abstraction (LiteLLM adapter), auth types (api_key/oauth/custom_header/none), presets (PROVIDER_PRESETS), runtime CRUD (ProviderManagementService with asyncio.Lock serialization), hot-reload via AppState swap.
Applied to files:
CLAUDE.mdsrc/synthorg/api/state.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/api/**/*.py : Authentication uses JWT + API key. Approval gate integration for high-risk operations.
Applied to files:
src/synthorg/api/auth/middleware.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/auth/service.pysrc/synthorg/api/state.pysrc/synthorg/api/auth/controller.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to {pyproject.toml,src/synthorg/__init__.py} : Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)
Applied to files:
src/synthorg/api/auth/__init__.pysrc/synthorg/api/errors.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to tests/**/*.py : Fix flaky tests completely and fundamentally; for timing-sensitive tests, mock `time.monotonic()` and `asyncio.sleep()` to make them deterministic instead of widening timing margins
Applied to files:
tests/unit/api/test_concurrency.pytests/unit/api/auth/test_session_store.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly from the domain module
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly from synthorg.observability.events.<domain>.
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Use event constants from `synthorg.observability.events.<domain>` (e.g., `API_REQUEST_STARTED` from `events.api`); import directly and log with structured kwargs: `logger.info(EVENT, key=value)`, never interpolated strings
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly rather than using string literals
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/api.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/settings/definitions/api.pysrc/synthorg/api/controllers/settings.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Applied to files:
src/synthorg/settings/definitions/api.pysrc/synthorg/api/controllers/settings.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves
Applied to files:
src/synthorg/api/auth/session.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state
Applied to files:
src/synthorg/api/auth/session.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/api/auth/session.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must import `from synthorg.observability import get_logger` and define `logger = get_logger(__name__)`
Applied to files:
src/synthorg/api/config.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability includes structured logging via `get_logger(__name__)`, correlation tracking, and log sinks.
Applied to files:
src/synthorg/api/config.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have `from synthorg.observability import get_logger` followed by `logger = get_logger(__name__)`.
Applied to files:
src/synthorg/api/config.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`
Applied to files:
src/synthorg/api/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`. Never use import logging / logging.getLogger() / print() in application code.
Applied to files:
src/synthorg/api/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Applied to files:
src/synthorg/observability/events/api.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to src/synthorg/**/*.py : Always use event constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
Applied to files:
src/synthorg/observability/events/api.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/observability/events/api.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.
Applied to files:
src/synthorg/api/controllers/settings.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`
Applied to files:
src/synthorg/api/controllers/settings.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/controllers/settings.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/api/auth/session_store.py
🔇 Additional comments (59)
CLAUDE.md (1)
91-91:api/package summary update is accurate and well-scoped.This line now reflects the implemented API surface (JWT session lifecycle, presence tracking, ETag/If-Match utilities, and TLS hosting configuration) and matches the code in
src/synthorg/api/concurrency.py:1-65,src/synthorg/api/auth/presence.py:1-76,src/synthorg/api/auth/session_store.py:155-177, andsrc/synthorg/api/auth/service.py:165-195.Based on learnings: “Applies to src/synthorg/**/*.py : Package structure … api/ (REST+WebSocket, Litestar) … JWT + API key + WS ticket auth … lifecycle helpers”.
src/synthorg/api/auth/service.py (1)
153-201: LGTM! Clean implementation of JWT session tracking.The
create_tokenmethod now correctly generates a uniquesession_idusinguuid.uuid4().hex(32-character hex string) and embeds it in the JWT via the standardjticlaim. The return type change totuple[str, int, str]is well-documented, and the docstring updates clearly explain the new session tracking capability.tests/unit/api/auth/test_service.py (2)
79-92: Thorough test coverage for the new session tracking feature.The test correctly validates:
- The 3-tuple return signature
session_idis a 32-character string (matchinguuid4().hexoutput)- The
jticlaim in the decoded JWT matches the returnedsession_id
94-131: Appropriate use of_for unused return values.The tests that don't need the
session_idcorrectly use_token, _, _ortoken, _, _unpacking, maintaining consistency with the new return signature while clearly indicating unused values.tests/unit/api/conftest.py (1)
173-174: Correct update to match the newcreate_tokenreturn signature.The
make_auth_headershelper appropriately unpacks the 3-tuple while only using thetokenvalue needed for building Authorization headers.tests/unit/api/auth/test_middleware.py (3)
80-80: LGTM - updated to match new return signature.
133-133: LGTM - consistent update for password change test.
177-177: LGTM - consistent update for deleted user test.tests/unit/persistence/sqlite/test_migrations.py (2)
27-27: Sessions table correctly added to expected schema.The
sessionstable addition aligns with the schema definition insrc/synthorg/persistence/sqlite/schema.sqland supports the new session management feature.
63-64: Appropriate indexes for session queries.The two indexes support the expected query patterns:
idx_sessions_user_id: Efficient lookup for listing a user's sessionsidx_sessions_expires_at: Efficient cleanup of expired sessionsdocs/design/page-structure.md (1)
197-197: Documentation correctly updated to reflect new session management endpoints.The Login page API surface now includes the session management endpoints (
GET /auth/sessions,DELETE /auth/sessions/{session_id},POST /auth/logout) that were added as part of this PR.src/synthorg/api/auth/middleware.py (1)
169-182: Well-implemented session revocation check with appropriate defensive coding.The implementation correctly:
- Uses O(1) in-memory lookup for the hot path
- Handles missing
_session_storegracefully (startup window, initialization failure)- Logs with structured kwargs before denying access
- Uses
hasattr+is not Noneguards per the lifecycle initialization pattern shown insrc/synthorg/api/lifecycle.pyOne consideration: tokens without a
jticlaim (e.g., older tokens issued before this change) will bypass the revocation check entirely due to theif jtiguard on line 172. This is likely intentional for backward compatibility, but be aware that such tokens cannot be individually revoked until they expire naturally.tests/unit/api/test_channels.py (1)
42-49: Dynamic user-scoped channels are properly protected by server-side access control.The change to
_arbitrary_channels_allowed = Truesafely enables dynamicuser:{id}channels. Server-side access control is already implemented insrc/synthorg/api/controllers/ws.py: the_user_may_receive_channel()function (lines 272–285) validates that only the owning user may receive events on user-scoped channels. This check is enforced before forwarding events to the client, preventing unauthorized subscription to other users' channels.docs/errors.md (1)
110-110: LGTM!The new error codes are correctly documented with accurate descriptions:
SESSION_REVOKED(1002) properly categorized under Authentication for revoked sessionsVERSION_CONFLICT(4002) properly categorized under Conflict for ETag/If-Match mismatchesBoth align with the implementation in
src/synthorg/api/errors.py.Also applies to: 133-133
docs/design/operations.md (1)
1124-1124: LGTM!The API surface documentation accurately reflects the new session management capabilities (list/revoke sessions, logout) while preserving existing endpoint documentation.
tests/unit/api/controllers/test_ws.py (2)
9-22: LGTM!The shared
_TEST_USERfixture is well-defined with appropriate role and auth method for testing WebSocket message handling. UsingHumanRole.CEOensures the test user has sufficient permissions for all channel subscriptions.
27-206: Test coverage is comprehensive.All
_handle_messageinvocations correctly pass the_TEST_USERargument, maintaining consistency with the updated handler signature that requires user context for access control decisions.src/synthorg/api/auth/__init__.py (1)
6-7: LGTM!The
SessionandSessionStoreexports follow the established re-export pattern, enabling clean imports likefrom synthorg.api.auth import Session, SessionStore. Consistent with the coding guideline that pure re-export modules don't require logging.Also applies to: 16-17
src/synthorg/api/app.py (2)
78-78: LGTM!The
API_SESSION_CLEANUPevent constant is correctly imported from the domain-specific observability module, following coding guidelines.
206-231: LGTM!The session cleanup integration is well-implemented:
- The
Nonecheck for_session_storeproperly handles the case beforeset_session_store()is called during startup (persrc/synthorg/api/state.py:168)- Separate try/except block isolates session cleanup failures from ticket cleanup
- Error handling pattern (re-raising
MemoryError,RecursionError) is consistent with existing code- Uses PEP 758 except syntax correctly per coding guidelines
src/synthorg/api/server.py (2)
52-68: LGTM! Clean conditional configuration.The TLS and proxy configuration follows a clean pattern:
- TLS kwargs are only populated when
ssl_certfileis present (pairwise validation inServerConfigensuresssl_keyfileis also set)- Proxy kwargs are only populated when
trusted_proxiesis configured- The
API_TLS_CONFIGUREDevent provides useful observability
71-83: All uvicorn parameters are valid. Thessl_certfile,ssl_keyfile,ssl_ca_certs,forwarded_allow_ips, andproxy_headersparameters are all supported by uvicorn.run().tests/unit/api/test_errors.py (1)
27-27: LGTM!The test expectations correctly include the new error codes matching the
ErrorCodeenum definition:
SESSION_REVOKED(1002) in the auth category (1xxx)VERSION_CONFLICT(4002) in the conflict category (4xxx)These additions ensure the exhaustiveness tests (
test_has_all_codes,test_no_duplicate_values) properly validate the updated enum.Also applies to: 35-35
tests/unit/api/test_concurrency.py (1)
1-55: LGTM! Comprehensive unit tests for concurrency utilities.The test suite thoroughly validates the ETag/If-Match concurrency mechanism:
TestComputeEtag: Verifies determinism, sensitivity to both value and timestamp changes, RFC 7232 weak ETag format (W/"..."), and expected length (16 hex chars from SHA-256 truncation)TestCheckIfMatch: Covers the match/mismatch/bypass behavior, including the important edge cases whereNoneor empty string ETags skip validation (consistent with "If-Match absent = bypass" per PR objectives)Good use of
pytest.mark.unitmarker as required by coding guidelines.tests/unit/api/test_config.py (1)
75-131: Good coverage for TLS pair and trusted-proxy validation paths.These tests exercise defaults and invalid combinations well, including CA-without-cert and proxy tuple handling.
src/synthorg/api/config.py (1)
179-210: Validator and exposure warning behavior look solid.The cert/key/CA constraints are enforced clearly, and the structured warning for unsafe
0.0.0.0exposure is a good safety signal.src/synthorg/api/errors.py (1)
202-215:VersionConflictErrormapping is consistent.409 status, conflict category, and
VERSION_CONFLICTcode are aligned and cleanly implemented.tests/unit/api/test_server.py (1)
48-89: Nice parameter-plumbing coverage for TLS and proxy settings.These assertions are targeted and verify both default omission and configured inclusion paths.
tests/unit/api/auth/test_presence.py (1)
10-62: Presence tracker tests are comprehensive for expected lifecycle behavior.Good coverage of multi-tab counting and idempotent/safe disconnect behavior.
src/synthorg/api/auth/presence.py (1)
24-75: Implementation is clean and correct.The counter logic correctly handles multi-tab scenarios. Returning
frozensetfromonline_users()prevents external mutation—good defensive design.src/synthorg/api/auth/session.py (1)
9-40: Well-designed frozen domain model.The
Sessionmodel correctly usesfrozen=True,allow_inf_nan=False,NotBlankStrfor identifiers, andAwareDatetimefor timestamps. The choice to allow emptyip_address/user_agentaligns with the schema defaults and handles edge cases where client metadata is unavailable.src/synthorg/settings/definitions/api.py (1)
61-121: TLS settings are well-structured.Good security practice marking
ssl_keyfileassensitive=True. The empty defaults allow TLS to remain optional, andrestart_required=Truecorrectly reflects that these are bootstrap-time configurations.src/synthorg/persistence/sqlite/schema.sql (1)
202-217: Sessions table schema is sound.The schema correctly models session lifecycle with
ON DELETE CASCADEfor user removal, appropriate defaults, and indexes onuser_idandexpires_atto support listing and cleanup queries. Therevoked INTEGERpattern (0/1) is idiomatic SQLite for booleans.src/synthorg/api/controllers/ws.py (3)
271-286: Access control logic is correct.The
_channel_allowed()function properly enforces:
- User channels restricted to owner via
extract_user_id()- Budget channels restricted to CEO/MANAGER
- Other channels implicitly allowed (gated by prior
_check_ws_rolewhich requires_READ_ROLES)
432-434: Presence lifecycle is correctly wired.
connect()is called after successful setup, anddisconnect()is in thefinallyblock ensuring cleanup on any exit path (normal disconnect, errors, or exceptions).Also applies to: 497-501
644-669: Secure subscription validation.Silently dropping unauthorized user-channel subscriptions without revealing whether the target user exists is the right security posture. The warning log enables security monitoring.
src/synthorg/api/channels.py (2)
50-85: Helper functions are well-designed.Clean separation of concerns:
user_channel()constructs,is_user_channel()detects, andextract_user_id()extracts with a safeNonereturn for non-user channels.
156-170: Enabling arbitrary channels is appropriate for user-scoped channels.The docstring correctly documents that server-side access control (in
ws.py) enforces which channels each user can subscribe to. Thebus_bridgecan publish touser:{id}channels for server-initiated notifications, which is the intended design.src/synthorg/observability/events/api.py (1)
84-100: Event constants follow established conventions.The new constants are well-organized by domain (session, network, concurrency, WebSocket) and follow the
API_<DOMAIN>_<ACTION>naming pattern.tests/unit/api/auth/test_session_store.py (2)
65-106: Test fixture setup is solid.The
dbfixture correctly applies the schema, seeds prerequisite users for FK constraints, and uses in-memory SQLite for isolation. The deterministic_NOWtimestamp via patching ensures reproducible tests.
115-333: Comprehensive test coverage.Tests cover all
SessionStoreoperations including edge cases (nonexistent sessions, already-revoked, expired sessions). Theload_revoked_restores_statetest correctly simulates a restart scenario by creating a new store instance.src/synthorg/api/auth/controller.py (5)
1-38: LGTM on imports and module setup.The imports are well-organized, appropriate event constants are imported from the observability module, and
logger = get_logger(__name__)follows the coding guidelines.
179-193: LGTM on SessionResponse DTO.Follows frozen Pydantic model conventions with
allow_inf_nan=False, usesNotBlankStrfor identifiers, and appropriately serializes timestamps as ISO strings for API responses.
407-415: LGTM on setup session integration.Session creation is correctly placed after user persistence and race-guard check, with the new
create_tokenreturn signature properly unpacked.
474-482: LGTM on login session integration.Session creation is correctly placed after successful credential verification, consistent with the setup flow.
754-767: Good security practice: merged 404 response.Returning 404 for both not-found and not-owned sessions prevents session ID enumeration attacks. The CEO bypass for administrative revocation is correctly implemented.
src/synthorg/api/controllers/settings.py (3)
269-299: LGTM on get_setting ETag addition.ETag header is correctly computed from
entry.valueandentry.updated_at, enabling clients to use If-Match for optimistic concurrency on subsequent updates.
338-345: LGTM on update response with new ETag.Returning the updated ETag in the response header enables clients to chain subsequent updates without a separate GET request.
456-458: LGTM on PEP 758 except syntax.Correctly uses Python 3.14's
except MemoryError, RecursionError:syntax per coding guidelines.src/synthorg/api/auth/session_store.py (10)
1-22: LGTM on module setup.Proper docstring explains the hybrid architecture, imports are appropriate, and logger follows coding guidelines.
43-56: LGTM on SessionStore initialization.Clean initialization with proper docstring documenting the
row_factoryexpectation for the database connection.
58-67: LGTM on load_revoked.Per context snippet 2 (lifecycle.py:217-226),
load_revoked()is called immediately after construction and beforeset_session_store(), eliminating the race window whereis_revoked()could return false negatives.
69-98: LGTM on create method.Parameterized SQL query, immediate commit for durability, and DEBUG-level logging for session creation.
116-153: LGTM on list methods.Both
list_by_userandlist_allcorrectly filter for active (non-revoked, non-expired) sessions with proper ordering.
155-177: LGTM on revoke method.Correctly persists to SQLite before updating in-memory set, ensuring durability. The
WHERE revoked = 0clause prevents duplicate logging.
179-212: LGTM on revoke_all_for_user.The re-query pattern correctly handles the race where new sessions could be created between UPDATE and in-memory sync. Using
set.update()is idempotent for already-present IDs.
214-225: LGTM on is_revoked.Synchronous O(1) set membership check is optimal for the auth middleware hot path.
227-254: LGTM on cleanup_expired.Correctly removes expired sessions from both SQLite and the in-memory revocation set. The SELECT-then-DELETE pattern is appropriate since any sessions expiring during the operation will be cleaned up in the next scheduled run.
34-38: No action required — the timestamp deserialization is correct.
datetime.fromisoformat()properly handles the timezone-aware ISO format (+00:00) thatdatetime.now(UTC).isoformat()produces. All timestamp values stored in the database come fromSessionfields (which areAwareDatetime), ensuring they include timezone information. Pydantic'sAwareDatetimevalidation succeeds with the parsed timezone-aware datetime objects.
| # 1xxx -- auth | ||
| UNAUTHORIZED = 1000 | ||
| FORBIDDEN = 1001 | ||
| SESSION_REVOKED = 1002 |
There was a problem hiding this comment.
SESSION_REVOKED is defined but not surfaced by auth failures.
The new code at Line 43 won’t reach clients unless a dedicated error path emits it. Current revoked-session handling still maps to generic unauthorized responses, so API consumers cannot distinguish revoked tokens.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/errors.py` at line 43, SESSION_REVOKED (1002) is defined but
never returned to clients; update the authentication failure path to emit this
specific error code when a token is revoked. Locate the token verification /
auth middleware (e.g., functions named verify_token, authenticate_request, or
the auth exception handler) and add a branch that detects revoked tokens and
raises/returns an API error that includes SESSION_REVOKED (1002) as the error
code instead of the generic unauthorized response; ensure the emitted error uses
the same error response schema used elsewhere so clients can distinguish revoked
sessions.
src/synthorg/api/lifecycle.py
Outdated
| except Exception: | ||
| logger.warning( | ||
| API_APP_STARTUP, | ||
| error="Session store init failed (non-fatal)", | ||
| exc_info=True, | ||
| ) |
There was a problem hiding this comment.
Fail-open session-store startup weakens revocation guarantees.
On Line 224, session store initialization failure is treated as non-fatal, which allows startup to continue without revocation checks. That makes logout/revocation ineffective for JWT jti validation paths.
🔧 Proposed fix
- except Exception:
- logger.warning(
- API_APP_STARTUP,
- error="Session store init failed (non-fatal)",
- exc_info=True,
- )
+ except Exception:
+ logger.exception(
+ API_APP_STARTUP,
+ error="Session store init failed",
+ )
+ raise📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except Exception: | |
| logger.warning( | |
| API_APP_STARTUP, | |
| error="Session store init failed (non-fatal)", | |
| exc_info=True, | |
| ) | |
| except Exception: | |
| logger.exception( | |
| API_APP_STARTUP, | |
| error="Session store init failed", | |
| ) | |
| raise |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/lifecycle.py` around lines 224 - 229, The current except
Exception block around session store init (logged with API_APP_STARTUP) treats
failures as non-fatal, which permits startup without revocation support; change
it to treat initialization failure as fatal: log the error (include
exc_info/exception details) at error level and either re-raise the exception or
terminate startup (e.g., sys.exit(1)) so the app does not start without a
functioning session store used by JWT jti revocation checks; update the except
block that wraps the session store init and replace logger.warning with
logger.error and a fatal fail/raise to prevent fail-open behavior.
c3551b2 to
ab0e341
Compare
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
tests/unit/api/auth/test_middleware.py (1)
57-68: 🧹 Nitpick | 🔵 TrivialMissing test coverage for session revocation path.
has_session_store = Falseensures all existing tests bypass the revocation check, which is correct for testing other middleware paths. However, there's no test that verifies the revocation guard works whenhas_session_store = Trueand the session is revoked. Consider adding a test that:
- Sets
has_session_store = Truewith a mocksession_store- Configures
is_revoked(jti)to returnTrue- Asserts 401 response
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/api/auth/test_middleware.py` around lines 57 - 68, The tests currently never exercise the session revocation branch because _FakeState sets has_session_store = False; add a new unit test that constructs an app like the existing one but sets _FakeState.has_session_store = True, injects a mock session_store on the fake state, configures the mock's is_revoked to return True for the test jti, invokes the protected_route through middleware_cls (using the same auth_service/session context), and assert the response status is 401 to verify the revocation guard path is enforced.src/synthorg/settings/service.py (1)
466-544:⚠️ Potential issue | 🟡 MinorMissing log statement before raising
VersionConflictError.Per coding guidelines, error paths should be logged at WARNING or ERROR before raising. The concurrent modification case at Lines 538-544 raises
VersionConflictErrorwithout logging, unlike other error paths in this method.🐛 Proposed fix
if not written: from synthorg.api.errors import ( # noqa: PLC0415 VersionConflictError, ) + logger.warning( + SETTINGS_VALUE_SET, + namespace=namespace, + key=key, + error="Concurrent modification detected", + ) msg = f"Concurrent modification on {namespace}/{key}" raise VersionConflictError(msg)As per coding guidelines: "Log all error paths at WARNING or ERROR in Python before raising; provide context in log messages."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/settings/service.py` around lines 466 - 544, The concurrent-modification branch in the async method set (when written is False after calling self._repository.set) raises VersionConflictError without logging; add a log call (e.g., logger.warning or logger.error) immediately before raising that includes the namespace, key, expected_updated_at and updated_at/context and a short message like "Concurrent modification on {namespace}/{key}" to match the other error paths (see existing use of logger.warning with SETTINGS_NOT_FOUND and SETTINGS_VALIDATION_FAILED and logger.error with SETTINGS_ENCRYPTION_ERROR) so the error is recorded before raising VersionConflictError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/auth/presence.py`:
- Around line 27-32: Update the public identifier type hints to use NotBlankStr
from core.types: change the connect(self, user_id: str), disconnect(self,
user_id: str), get_active_connections(self, user_id: str) and is_online(self,
user_id: str) signatures to use user_id: NotBlankStr, add/import NotBlankStr in
this module, and adjust any related docstrings/type comments to reflect
NotBlankStr for the user identifier parameters so the boundary enforces
non-empty/non-whitespace IDs.
- Around line 34-60: Update the presence logging to use the domain event
constants from synthorg.observability.events and log at INFO level: in the
connect and disconnect methods of the presence class (the methods named connect
and disconnect in src/synthorg/api/auth/presence.py) replace logger.debug calls
with logger.info and pass the appropriate event constants (e.g.,
synthorg.observability.events.USER_PRESENCE_CONNECT and
synthorg.observability.events.USER_PRESENCE_DISCONNECT) while retaining the
structured fields (user_id and count); add the necessary import for those
constants at the top of the module.
In `@src/synthorg/api/auth/session.py`:
- Around line 45-50: The model validator _validate_temporal_ordering on the
Session model currently only checks created_at <= expires_at; extend it to also
validate that last_active_at (if present) falls within the session lifetime by
ensuring created_at <= last_active_at <= expires_at, and raise a ValueError with
a clear message (e.g., "last_active_at must be between created_at and
expires_at") when the check fails; update the validator logic in
_validate_temporal_ordering to perform these comparisons on the Session fields
created_at, last_active_at, and expires_at.
- Around line 24-25: The docstring states user_agent is capped at 512 characters
but nothing enforces it; add validation at the session boundary by checking the
user_agent parameter in the session creation path (e.g., Session.__init__ or the
create_session/login_user function) and either truncate it to 512 characters or
raise a ValueError when it exceeds 512; ensure the validation happens before the
value is stored on the session object and include a short warning log if you
truncate.
In `@src/synthorg/api/channels.py`:
- Around line 50-59: The user_channel helper currently allows empty user IDs
(producing "user:")—update its signature to accept core.types.NotBlankStr
(import NotBlankStr) and enforce/reject empty suffixes at the helper boundary
(raise ValueError if the provided value is blank) so malformed channels cannot
be created; likewise update the complementary extractor function extract_user_id
and any other channel helpers in the same file (the ones around lines 74-85) to
accept/return NotBlankStr (or NotBlankStr | None for optional cases), validate
inputs, and consistently raise on invalid/blank identifiers rather than
returning empty strings.
In `@src/synthorg/api/concurrency.py`:
- Around line 40-68: The check_if_match function currently only does string
equality and logs conflicts at INFO; update it to handle full If-Match syntax:
treat a request_etag of "*" as a match, and parse comma-separated entity-tag
lists (strip whitespace and optional surrounding quotes) to check if
current_etag is present; if no match, call logger.warning with
API_CONCURRENCY_CONFLICT and the same context (resource, request_etag,
current_etag) and then raise VersionConflictError (include a descriptive message
mentioning resource/current_etag). Ensure you update check_if_match to skip when
request_etag is empty as before, but otherwise perform the wildcard/list parsing
and use warning-level logging before raising the error.
In `@src/synthorg/api/config.py`:
- Around line 208-231: The code currently treats any non-empty trusted_proxies
as safe and misses IPv6 wildcard binds, so update the validation in the
trusted_proxies loop (and the subsequent host check using self.host and
API_NETWORK_EXPOSURE_WARNING) to detect and reject overly-broad entries like
0.0.0.0/0 or ::/0: parse each entry with ipaddress.ip_network(entry,
strict=False) and if network.prefixlen == 0 (or the network covers the entire
address family) raise ValueError (or at minimum mark as unsafe) so the
network-exposure warning is not suppressed; also extend the host wildcard check
to include '::' (IPv6) in addition to '0.0.0.0' so binding to all interfaces
triggers the warning unless a legitimately narrow trusted_proxies list is
present.
In `@src/synthorg/api/controllers/ws.py`:
- Around line 644-657: The warning currently logs the raw probed channel string
(variable c) in the denial path which may contain another user's identifier;
update the denial logging in the block that checks is_user_channel(c) so it does
not persist the full user:<id> value — either redact the identifier portion
(e.g. replace everything after the prefix returned by user_channel(...) with a
fixed token) or log a deterministic hash of c instead; modify the logger.warning
call (API_WS_USER_CHANNEL_DENIED) to include conn_user.user_id and the
redacted/hashed form of c (but never the raw c), leaving the rest of the control
flow (valid list, subscribed.update) unchanged.
- Around line 497-502: Move presence disconnect into an unconditional teardown
so it runs even if unsubscribe fails: call app_state =
socket.app.state["app_state"] and
app_state.user_presence.disconnect(user.user_id) from a finally block that
always executes, and wrap await channels_plugin.unsubscribe(subscriber) in its
own try/except to catch and log unsubscribe errors (use logger.error or similar)
rather than letting that exception prevent the disconnect; keep the existing
logger.info(API_WS_DISCONNECTED, client=str(socket.client)) after successful
disconnect or in the outer finally as appropriate.
In `@src/synthorg/api/state.py`:
- Around line 456-472: Update set_session_store to log the duplicate-assignment
error before raising and to use the repository event constants instead of a raw
string: import the relevant constants (e.g. EVENT_SESSION_STORE_CONFIGURED and
EVENT_SESSION_STORE_ALREADY_CONFIGURED) from synthorg.observability.events, call
logger.warning(...) with EVENT_SESSION_STORE_ALREADY_CONFIGURED and a
descriptive note immediately before raising RuntimeError when
self._session_store is not None, and replace the raw
"app.state.session_store_set" in the success path with
logger.info(EVENT_SESSION_STORE_CONFIGURED, note="Session store configured").
In `@src/synthorg/persistence/protocol.py`:
- Line 7: Change the get_db() signature in the Protocol to return a concrete
typed contract instead of Any: define or reference a DB connection Protocol/ABC
(e.g., DBConnectionProtocol) and use that as the return type for get_db();
update the Protocol import and annotations accordingly (replace Any with
DBConnectionProtocol or at minimum object) and add a docstring on get_db() that
callers must expect PersistenceConnectionError for connection failures and
NotImplementedError for unsupported backends; apply the same stricter typing to
the related public functions mentioned (lines ~101-110) so mypy strict mode
passes.
In `@src/synthorg/persistence/sqlite/schema.sql`:
- Around line 216-218: Add a new composite index on sessions(revoked,
expires_at) because the load_revoked() and the global active-sessions query in
session_store.py filter by revoked and expires_at without user_id; create an
index like idx_sessions_revoked_expires on (revoked, expires_at) in schema.sql
so those global WHERE revoked = ? AND expires_at > ? queries can use an index
and avoid full table scans.
In `@tests/unit/api/fakes.py`:
- Around line 604-606: Update the return type of FakePersistenceBackend.get_db
to match the protocol by changing its annotation from object to Any: modify the
signature of get_db in the FakePersistenceBackend class to return typing.Any
(import Any from typing if not already imported) and keep the same
NotImplementedError body and message so the fake still raises as before.
---
Outside diff comments:
In `@src/synthorg/settings/service.py`:
- Around line 466-544: The concurrent-modification branch in the async method
set (when written is False after calling self._repository.set) raises
VersionConflictError without logging; add a log call (e.g., logger.warning or
logger.error) immediately before raising that includes the namespace, key,
expected_updated_at and updated_at/context and a short message like "Concurrent
modification on {namespace}/{key}" to match the other error paths (see existing
use of logger.warning with SETTINGS_NOT_FOUND and SETTINGS_VALIDATION_FAILED and
logger.error with SETTINGS_ENCRYPTION_ERROR) so the error is recorded before
raising VersionConflictError.
In `@tests/unit/api/auth/test_middleware.py`:
- Around line 57-68: The tests currently never exercise the session revocation
branch because _FakeState sets has_session_store = False; add a new unit test
that constructs an app like the existing one but sets
_FakeState.has_session_store = True, injects a mock session_store on the fake
state, configures the mock's is_revoked to return True for the test jti, invokes
the protected_route through middleware_cls (using the same auth_service/session
context), and assert the response status is 401 to verify the revocation guard
path is enforced.
🪄 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: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7b7f1d96-5643-45b6-b055-56b547ab640d
📒 Files selected for processing (44)
CLAUDE.mddocs/design/operations.mddocs/design/page-structure.mddocs/errors.mdsrc/synthorg/api/app.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/auth/controller.pysrc/synthorg/api/auth/middleware.pysrc/synthorg/api/auth/presence.pysrc/synthorg/api/auth/service.pysrc/synthorg/api/auth/session.pysrc/synthorg/api/auth/session_store.pysrc/synthorg/api/channels.pysrc/synthorg/api/concurrency.pysrc/synthorg/api/config.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/errors.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/server.pysrc/synthorg/api/state.pysrc/synthorg/observability/events/api.pysrc/synthorg/persistence/protocol.pysrc/synthorg/persistence/repositories.pysrc/synthorg/persistence/sqlite/backend.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/service.pytests/unit/api/auth/test_controller.pytests/unit/api/auth/test_middleware.pytests/unit/api/auth/test_presence.pytests/unit/api/auth/test_service.pytests/unit/api/auth/test_session_store.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/fakes.pytests/unit/api/test_channels.pytests/unit/api/test_concurrency.pytests/unit/api/test_config.pytests/unit/api/test_errors.pytests/unit/api/test_server.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/persistence/test_protocol.py
| def connect(self, user_id: str) -> None: | ||
| """Record a new WebSocket connection for a user. | ||
|
|
||
| Args: | ||
| user_id: The connecting user's ID. | ||
| """ |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use NotBlankStr for user identifier parameters in public methods.
user_id is a public identifier field but is typed as str. Please switch these method signatures to NotBlankStr to enforce non-empty/non-whitespace IDs at the boundary.
♻️ Proposed type-hint update
+from synthorg.core.types import NotBlankStr
from synthorg.observability import get_logger
logger = get_logger(__name__)
@@
- def connect(self, user_id: str) -> None:
+ def connect(self, user_id: NotBlankStr) -> None:
@@
- def disconnect(self, user_id: str) -> None:
+ def disconnect(self, user_id: NotBlankStr) -> None:
@@
- def is_online(self, user_id: str) -> bool:
+ def is_online(self, user_id: NotBlankStr) -> bool:
@@
- def connection_count(self, user_id: str) -> int:
+ def connection_count(self, user_id: NotBlankStr) -> int:As per coding guidelines: "Use NotBlankStr from core.types for all identifier/name fields in Python."
Also applies to: 40-45, 62-67, 81-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/presence.py` around lines 27 - 32, Update the public
identifier type hints to use NotBlankStr from core.types: change the
connect(self, user_id: str), disconnect(self, user_id: str),
get_active_connections(self, user_id: str) and is_online(self, user_id: str)
signatures to use user_id: NotBlankStr, add/import NotBlankStr in this module,
and adjust any related docstrings/type comments to reflect NotBlankStr for the
user identifier parameters so the boundary enforces non-empty/non-whitespace
IDs.
| logger.debug( | ||
| "user.presence.connect", | ||
| user_id=user_id, | ||
| count=self._counts[user_id], | ||
| ) | ||
|
|
||
| def disconnect(self, user_id: str) -> None: | ||
| """Record a WebSocket disconnection for a user. | ||
|
|
||
| Args: | ||
| user_id: The disconnecting user's ID. | ||
| """ | ||
| count = self._counts.get(user_id, 0) - 1 | ||
| if count <= 0: | ||
| self._counts.pop(user_id, None) | ||
| logger.debug( | ||
| "user.presence.disconnect", | ||
| user_id=user_id, | ||
| count=0, | ||
| ) | ||
| else: | ||
| self._counts[user_id] = count | ||
| logger.debug( | ||
| "user.presence.disconnect", | ||
| user_id=user_id, | ||
| count=count, | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use observability event constants and INFO for presence state transitions.
These logs currently use inline string event names and DEBUG. Presence connect/disconnect are state transitions and should use domain event constants at INFO level for consistency and downstream routing.
As per coding guidelines: "Event names: always use constants from the domain-specific module under synthorg.observability.events" and "All state transitions must log at INFO level in Python application code."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/presence.py` around lines 34 - 60, Update the presence
logging to use the domain event constants from synthorg.observability.events and
log at INFO level: in the connect and disconnect methods of the presence class
(the methods named connect and disconnect in src/synthorg/api/auth/presence.py)
replace logger.debug calls with logger.info and pass the appropriate event
constants (e.g., synthorg.observability.events.USER_PRESENCE_CONNECT and
synthorg.observability.events.USER_PRESENCE_DISCONNECT) while retaining the
structured fields (user_id and count); add the necessary import for those
constants at the top of the module.
| user_agent: Client User-Agent header at login time | ||
| (capped at 512 characters). |
There was a problem hiding this comment.
The documented 512-character user_agent cap is not enforced.
The docstring says this field is capped at 512 characters, but the model currently accepts arbitrary length. Because User-Agent is client-controlled, that leaves the bound to caller discipline instead of enforcing it at the session boundary. As per coding guidelines, "Validate at system boundaries in Python (user input, external APIs, config files)."
Also applies to: 39-39
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/session.py` around lines 24 - 25, The docstring states
user_agent is capped at 512 characters but nothing enforces it; add validation
at the session boundary by checking the user_agent parameter in the session
creation path (e.g., Session.__init__ or the create_session/login_user function)
and either truncate it to 512 characters or raise a ValueError when it exceeds
512; ensure the validation happens before the value is stored on the session
object and include a short warning log if you truncate.
| def user_channel(user_id: str) -> str: | ||
| """Return the user-scoped channel name. | ||
|
|
||
| Args: | ||
| user_id: The user's unique identifier. | ||
|
|
||
| Returns: | ||
| Channel name like ``user:abc123``. | ||
| """ | ||
| return f"{CHANNEL_USER_PREFIX}{user_id}" |
There was a problem hiding this comment.
Reject empty user IDs at the channel-helper boundary.
user_channel("") currently produces user:, and extract_user_id("user:") returns an empty identifier instead of treating the channel as invalid. Tighten these helpers to use NotBlankStr and reject an empty suffix so malformed private channel names never get created. As per coding guidelines, "Use NotBlankStr from core.types for all identifier/name fields in Python; supports optional (NotBlankStr | None) and tuple variants."
Also applies to: 74-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/channels.py` around lines 50 - 59, The user_channel helper
currently allows empty user IDs (producing "user:")—update its signature to
accept core.types.NotBlankStr (import NotBlankStr) and enforce/reject empty
suffixes at the helper boundary (raise ValueError if the provided value is
blank) so malformed channels cannot be created; likewise update the
complementary extractor function extract_user_id and any other channel helpers
in the same file (the ones around lines 74-85) to accept/return NotBlankStr (or
NotBlankStr | None for optional cases), validate inputs, and consistently raise
on invalid/blank identifiers rather than returning empty strings.
| def set_session_store(self, store: SessionStore) -> None: | ||
| """Set the session store (deferred initialisation). | ||
|
|
||
| Args: | ||
| store: Configured session store. | ||
|
|
||
| Raises: | ||
| RuntimeError: If the session store has already been set. | ||
| """ | ||
| if self._session_store is not None: | ||
| msg = "session_store is already configured" | ||
| raise RuntimeError(msg) | ||
| self._session_store = store | ||
| logger.info( | ||
| "app.state.session_store_set", | ||
| note="Session store configured", | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Align set_session_store() with the repo's logging conventions.
The duplicate-assignment branch raises without logging, and the success path emits a raw event string instead of an imported event constant. That makes this startup/state-transition path inconsistent with the rest of AppState. As per coding guidelines, "Always use event name constants from synthorg.observability.events domain-specific modules in Python; import directly" and "Log all error paths at WARNING or ERROR in Python before raising."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/state.py` around lines 456 - 472, Update set_session_store
to log the duplicate-assignment error before raising and to use the repository
event constants instead of a raw string: import the relevant constants (e.g.
EVENT_SESSION_STORE_CONFIGURED and EVENT_SESSION_STORE_ALREADY_CONFIGURED) from
synthorg.observability.events, call logger.warning(...) with
EVENT_SESSION_STORE_ALREADY_CONFIGURED and a descriptive note immediately before
raising RuntimeError when self._session_store is not None, and replace the raw
"app.state.session_store_set" in the success path with
logger.info(EVENT_SESSION_STORE_CONFIGURED, note="Session store configured").
| """ | ||
|
|
||
| from typing import Protocol, runtime_checkable | ||
| from typing import Any, Protocol, runtime_checkable |
There was a problem hiding this comment.
Tighten get_db() contract: avoid Any and document unsupported-backend behavior.
get_db() currently returns Any and documents only PersistenceConnectionError, but implementations in tests use NotImplementedError to signal unsupported backends. Please make the protocol explicit to avoid ambiguous caller handling.
🛠️ Proposed contract/type refinement
-from typing import Any, Protocol, runtime_checkable
+from typing import Protocol, runtime_checkable
@@
- def get_db(self) -> Any:
+ def get_db(self) -> object:
@@
Raises:
+ NotImplementedError: If the backend does not expose a raw DB handle.
PersistenceConnectionError: If not yet connected.As per coding guidelines: "Type hints: all public functions, mypy strict mode."
Also applies to: 101-110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/protocol.py` at line 7, Change the get_db()
signature in the Protocol to return a concrete typed contract instead of Any:
define or reference a DB connection Protocol/ABC (e.g., DBConnectionProtocol)
and use that as the return type for get_db(); update the Protocol import and
annotations accordingly (replace Any with DBConnectionProtocol or at minimum
object) and add a docstring on get_db() that callers must expect
PersistenceConnectionError for connection failures and NotImplementedError for
unsupported backends; apply the same stricter typing to the related public
functions mentioned (lines ~101-110) so mypy strict mode passes.
- TLS configuration (ssl_certfile, ssl_keyfile, ssl_ca_certs) with
pairwise validation and uvicorn passthrough
- Trusted proxy support (forwarded_allow_ips, proxy_headers) for
reverse proxy deployments
- JWT session tracking with jti claim, SQLite-backed session store,
and in-memory revocation set for O(1) middleware checks
- Session CRUD endpoints (GET /auth/sessions, DELETE /auth/sessions/{id},
POST /auth/logout) with CEO admin scope
- Session revocation check in auth middleware (sync, non-blocking)
- ETag/If-Match optimistic concurrency on settings endpoints with
SHA-256 weak ETags and 409 conflict responses
- Per-user WebSocket channels (user:{id}) with server-side isolation
and role-based event filtering (budget events CEO/Manager only)
- User presence tracking (in-memory connection counting for multi-tab)
- New error codes: SESSION_REVOKED (1002), VERSION_CONFLICT (4002)
- New VersionConflictError class for ETag conflicts
- 15 new API settings for TLS and trusted proxy configuration
- Sessions table with user FK, expiry index, and revocation flag
Closes #244
- Fix _patch_now return type with @contextmanager pattern - Fix _extract_jti Any return type with else block (TRY300) - Fix lifecycle _db access with type: ignore[attr-defined] - Fix test_server ApiConfig construction for strict typing
Pre-reviewed by 8 agents, 18 findings addressed: - Wrap _create_session store.create() in try/except (best-effort) - Wire UserPresence into AppState slots + property (was dead code) - Fix session cleanup comment (runs every 60s, not every 5 min) - Fix session cleanup event constant (API_SESSION_CLEANUP) - Narrow _extract_jti except to jwt.PyJWTError + log unexpected - Merge 404/403 in revoke_session to prevent session ID enumeration - Fix TOCTOU in revoke_all_for_user (UPDATE first, then re-query) - Add user_id/username to SessionResponse for CEO scope=all view - Update channels docstring for arbitrary_channels_allowed=True - Rename _BUDGET_CHANNELS to BUDGET_CHANNELS (public API) - Extract _check_setting_etag helper (update_setting under 50 lines) - Remove 3 unused event constants - Update CLAUDE.md Package Structure with new modules - Update operations.md API Surface with session/logout endpoints - Update docs/errors.md with SESSION_REVOKED and VERSION_CONFLICT - Update page-structure.md with session/logout endpoints - Reduce controller.py to under 800 lines
…ini, and Copilot Security: - Strong ETags (RFC 7232 compliance) -- removed W/ prefix for If-Match - Require jti in JWT decoder -- tokens without jti are now rejected - SessionRevokedError class -- clients can distinguish revoked sessions - Validate trusted_proxies as IP/CIDR at config time - Normalize empty TLS strings to None (prevents bypass of exposure warning) - Cap user_agent at 512 chars in session creation - Truncate jti in revocation log to first 8 chars - Log not-owned session revocation attempts for security monitoring - Validate scope parameter in list_sessions endpoint Correctness: - Atomic ETag compare-and-swap -- settings_service.set() and repository.set() accept expected_updated_at for TOCTOU-safe optimistic concurrency - Session store load_revoked filters expired sessions (bounded memory) - revoke_all_for_user re-query filters expired sessions - Session temporal ordering validator (created_at <= expires_at) - Guard set_session_store against reassignment + state transition log - has_session_store property on AppState (replaces private _session_store access) - Check store.revoke() return value in controller - Composite index (user_id, revoked, expires_at) for hot query path - Remove dead update_last_active method - Public get_db() on PersistenceBackend (replaces _db bypass in lifecycle) Code quality: - Type app_state as AppState in _create_session (was Any) - Use InvalidTokenError instead of PyJWTError in _extract_jti - SessionResponse uses AwareDatetime (was bare str) - Add API_SESSION_CREATE_FAILED event constant (was reusing success event) - Add DEBUG logging to UserPresence connect/disconnect - Add DEBUG log for PyJWTError in _extract_jti - Add DEBUG log for logout with no jti - Add WARNING logs on auth failures for 3 session endpoints - Fix 6 incorrect/stale docstrings and comments - Update settings definitions docstring (11 -> 15 settings) - AuthController docstring includes sessions and logout Tests: - Add 7 tests for WS channel access control (_channel_allowed, subscribe) - Add 6 tests for user_channel/is_user_channel/extract_user_id helpers - Add VersionConflictError and SessionRevokedError to error metadata test - Update ETag tests for strong format - Fix test payloads for jti requirement and temporal validator - Update FakePersistenceBackend and FakeSettingsRepository for new APIs
decode_token now requires jti (added in parent commit). All manually-crafted JWT payloads in test_controller.py and test_middleware.py must include the jti claim to avoid MissingRequiredClaimError.
- Extend Session temporal validator: created_at <= last_active_at <= expires_at - If-Match: support RFC 7232 wildcard (*) and comma-separated ETag lists - Reject overly broad trusted_proxies (0.0.0.0/0, ::/0); add :: to wildcard check - Redact probed user channel in WS denial log (was leaking target user ID) - Robust WS presence disconnect: inner try/except around unsubscribe - Log warning before raising on duplicate set_session_store - Add idx_sessions_revoked_expires index for global revoked queries - Log before raising VersionConflictError in settings service - Fix FakePersistenceBackend.get_db return type to Any - Add middleware session revocation test (was identified but not added)
ab0e341 to
83b6f65
Compare
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (5)
src/synthorg/api/auth/presence.py (2)
34-38:⚠️ Potential issue | 🟡 MinorOnly emit structured presence events for real transitions.
disconnect()logs a disconnect even when the user was never connected, and both methods still use inline string events atDEBUG. That makes presence telemetry inaccurate and misses the project logging contract. Use the domain event constants, log actual connect/disconnect transitions atINFO, and return early on the no-op disconnect path.As per coding guidelines: "All state transitions must log at INFO level" and "Always use structured logging with constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api), never use string formatting (logger.info(EVENT, key=value))."
Also applies to: 46-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/auth/presence.py` around lines 34 - 38, The presence logging should only emit structured domain events for real state transitions: update the connect(self, user_id) and disconnect(self, user_id) methods to use the domain event constants (e.g., events.presence.PRESENCE_CONNECTED and events.presence.PRESENCE_DISCONNECTED) instead of inline string messages, change their log level to INFO, and call logger.info(EVENT_CONST, user_id=user_id, count=self._counts[user_id]) when an actual transition occurs; additionally, make disconnect return early without logging if the user was never connected (no-op path) to avoid spurious disconnect events.
25-28: 🛠️ Refactor suggestion | 🟠 MajorUse
NotBlankStrthroughout the presence API.
UserPresencestill exposes user IDs as plainstrin its storage and public signatures/return type, which leaves this boundary inconsistent with the rest of the auth layer.As per coding guidelines: "Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators."
Also applies to: 40-40, 62-63, 73-74, 81-82
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/auth/presence.py` around lines 25 - 28, UserPresence currently uses plain str for user IDs in its storage and public method signatures; update it to use NotBlankStr from core.types everywhere: import NotBlankStr, change the _counts typing to dict[NotBlankStr, int], update method signatures for connect(user_id: NotBlankStr), disconnect(user_id: NotBlankStr), any other methods that accept/return user IDs (e.g., get_counts / all public APIs) to use NotBlankStr in parameters and return types, and adjust any internal annotations/usages accordingly so the presence API consistently enforces NotBlankStr.src/synthorg/api/state.py (1)
467-475: 🛠️ Refactor suggestion | 🟠 MajorUse dedicated event constants in
set_session_store().Both branches log the raw
"app.state.session_store_set"string here. That bypasses the repo’s event-constant convention and makes the failure and success paths indistinguishable in structured logs. Import dedicated constants for the configured / already-configured cases and use those instead.As per coding guidelines, “Always use structured logging with constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api), never use string formatting (logger.info(EVENT, key=value))”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/state.py` around lines 467 - 475, Replace the hard-coded "app.state.session_store_set" string with dedicated event constants: import the appropriate event names (e.g., SESSION_STORE_ALREADY_CONFIGURED and SESSION_STORE_CONFIGURED) from the synthorg.observability.events module used by the repo and use them in logger.warning and logger.info respectively inside set_session_store(); specifically, update the logger.warning call that signals the failure/already-configured path to use the SESSION_STORE_ALREADY_CONFIGURED constant and update the logger.info success call to use SESSION_STORE_CONFIGURED, and add the new imports at the top of the file.src/synthorg/api/auth/session.py (1)
23-25:⚠️ Potential issue | 🟠 MajorEnforce the documented
user_agentlength bound in the model.The docstring promises a 512-character cap, but
user_agentis still unbounded. Because this value is client-controlled and persisted/listed by the session APIs, oversized headers can bloat storage and responses. Add a field constraint here, or truncate before constructingSession.Minimal fix
-from pydantic import AwareDatetime, BaseModel, ConfigDict, model_validator +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator ... - user_agent: str + user_agent: str = Field(max_length=512)As per coding guidelines, “Validate at system boundaries: user input, external APIs, and config files”.
Also applies to: 39-39
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/auth/session.py` around lines 23 - 25, The Session model's user_agent field is not enforcing the documented 512-char cap; update the code that builds or defines Session (e.g., the Session dataclass/ORM model or the constructor used in create_session/Session.create) to enforce a maximum length of 512 characters by either adding a field constraint/validator or truncating the incoming user_agent value before constructing the Session instance (ensure the symbol names user_agent and Session are targeted so all session creation paths abide by the cap).src/synthorg/persistence/sqlite/settings_repo.py (1)
124-133:⚠️ Potential issue | 🟠 MajorSupport “expect no row” in the CAS path.
When
expected_updated_atis the empty-string sentinel used for resolved settings without a stored override, this branch can onlyUPDATE, so the first edit of an env/YAML/default-backed setting returnsFalseand becomes a 409 despite no concurrent write. Add an explicit insert-if-absent path here, or use a dedicated sentinel that distinguishes “no DB row yet” from “row exists with this timestamp”.Possible shape
- if expected_updated_at is not None: + if expected_updated_at == "": + cursor = await self._db.execute( + "INSERT INTO settings (namespace, key, value, updated_at) " + "SELECT ?, ?, ?, ? " + "WHERE NOT EXISTS (" + "SELECT 1 FROM settings WHERE namespace = ? AND key = ?" + ")", + (namespace, key, value, updated_at, namespace, key), + ) + await self._db.commit() + if cursor.rowcount == 0: + return False + elif expected_updated_at is not None: cursor = await self._db.execute(Based on learnings, settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/persistence/sqlite/settings_repo.py` around lines 124 - 133, The optimistic-CAS branch that checks expected_updated_at only issues an UPDATE, so when expected_updated_at is the empty-string sentinel (meaning “no DB row yet”) the first edit fails; modify the method handling expected_updated_at to detect the "no-row" sentinel and perform an insert-if-absent path: after the UPDATE returns zero rows, if expected_updated_at == "" then attempt to INSERT the (namespace, key, value, updated_at) row (or use a single SQL upsert/INSERT OR IGNORE then UPDATE) and treat a successful insert as CAS success; reference the symbols expected_updated_at, updated_at, namespace, key, value, cursor and self._db in settings_repo.py to locate where to add the insert/UPSERT branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/auth/controller.py`:
- Around line 734-746: The current handler silently falls back to list_by_user
when a non-CEO requests scope="all"; update the branch after the _VALID_SCOPES
check to make the behavior explicit: either (A) enforce denial by raising a 403
(e.g., raise ApiForbiddenError or similar) when scope == "all" and
auth_user.role != HumanRole.CEO so only CEOs can use store.list_all, or (B) keep
the safe fallback but set an explicit response header (e.g.,
response.headers["X-Scope-Applied"] = "own") before calling store.list_by_user
to indicate the reduced scope; implement the chosen change in the controller
where auth_user, _VALID_SCOPES, HumanRole.CEO, store.list_all and
store.list_by_user are referenced (i.e., immediately before calling
store.list_by_user).
In `@src/synthorg/api/auth/session_store.py`:
- Around line 98-102: The session lifecycle logs are using logger.debug for
state transitions; change the calls that log API_SESSION_CREATED (in function
handling session creation where logger.debug(API_SESSION_CREATED,
session_id=session.session_id, user_id=session.user_id)) and the corresponding
session cleanup log (the logger.debug that emits the cleanup constant, e.g.,
API_SESSION_CLEANED_UP) to logger.info while preserving the same message key and
structured fields (session_id, user_id) so these transitions emit at INFO level
to comply with the guidelines.
- Around line 54-56: The public methods on the session store accept plain str
for identifier parameters; update all public API parameter types to use
NotBlankStr (e.g., change session_id: str and user_id: str to session_id:
NotBlankStr and user_id: NotBlankStr) in the SessionStore class methods
referenced (including create, get/fetch, revoke/is_revoked and any methods
accepting tuples/optionals of IDs at the noted locations), and adjust any
tuple/optional type annotations to the corresponding NotBlankStr variants; keep
using the existing imported NotBlankStr and ensure internal uses still treat
values as strings where needed.
- Around line 198-214: The UPDATE in revoke_all_for_user currently sets
revoked=1 for all non-revoked rows regardless of expiry; change it to only
target active sessions by adding the same expires_at > ? predicate used later
(use now = datetime.now(UTC).isoformat()), bind (user_id, now) to the UPDATE
executed via self._db so the returned cursor.rowcount reflects only
newly-revoked active sessions, and keep updating self._revoked from the SELECT
that already uses expires_at > ?.
In `@src/synthorg/api/config.py`:
- Around line 182-189: The before-validator _normalize_empty_tls currently
mutates the incoming data mapping in place; instead make and operate on a copy
and return that to avoid side effects. Update _normalize_empty_tls to deep-copy
the incoming dict (use copy.deepcopy) into a new variable, perform the
empty-string-to-None normalization on that copy for keys ("ssl_certfile",
"ssl_keyfile", "ssl_ca_certs"), and return the copy; also ensure callers
ultimately receive an immutable mapping by wrapping the final constructed
mapping with MappingProxyType as per guidelines.
In `@src/synthorg/api/controllers/ws.py`:
- Around line 651-664: _handle_subscribe currently treats any channel in
_ALL_CHANNELS_SET as acceptable which causes non-privileged users to be
acknowledged for budget channels they will never receive; update the loop that
builds valid (iterating over channels and comparing to _ALL_CHANNELS_SET and
own_user_ch) to call _channel_allowed(conn_user, c) (or equivalent signature)
and only append c to valid if _channel_allowed returns true (preserve the
existing special-case check for own_user_ch and is_user_channel logging
behavior), so that subscribed and filters only include channels that _on_event
can actually deliver.
In `@src/synthorg/settings/service.py`:
- Around line 538-551: The code logs a SETTINGS_VALIDATION_FAILED event when a
write fails due to a version mismatch (variable written is False) but sets
reason="concurrent_modification", which dilutes observability; change the log
call in the block that raises VersionConflictError so it emits a dedicated event
constant (e.g., SETTINGS_VERSION_CONFLICT) instead of SETTINGS_VALIDATION_FAILED
and keep the metadata (namespace, key, expected_updated_at,
reason="concurrent_modification") intact; update any import or event registry
usage if needed and ensure the log location remains in the same branch where
written is falsy and VersionConflictError is raised.
In `@tests/unit/api/auth/test_session_store.py`:
- Around line 65-66: The db fixture's return annotation uses AsyncGenerator with
a single type parameter; update the type hint on the fixture function db to use
the full form AsyncGenerator[aiosqlite.Connection, None] (i.e., include the
second type parameter None) so static type checkers like mypy accept it; change
the annotation on the async def db(...) to AsyncGenerator[aiosqlite.Connection,
None] and adjust imports if necessary.
In `@tests/unit/api/test_channels.py`:
- Around line 55-73: The test currently accepts an empty user suffix for
extract_user_id (test_extract_user_id_empty_suffix), but after adding validation
in channels.py extract_user_id should reject "user:"; update the test to assert
the new behavior—either expect a ValueError to be raised when calling
extract_user_id("user:") or assert it returns None if you choose to signal
invalid channels that way—adjust test_extract_user_id_empty_suffix accordingly
and keep other tests (user_channel, is_user_channel,
test_extract_user_id_valid/non_user_channel) unchanged; reference
extract_user_id, user_channel, and is_user_channel when making the change.
---
Duplicate comments:
In `@src/synthorg/api/auth/presence.py`:
- Around line 34-38: The presence logging should only emit structured domain
events for real state transitions: update the connect(self, user_id) and
disconnect(self, user_id) methods to use the domain event constants (e.g.,
events.presence.PRESENCE_CONNECTED and events.presence.PRESENCE_DISCONNECTED)
instead of inline string messages, change their log level to INFO, and call
logger.info(EVENT_CONST, user_id=user_id, count=self._counts[user_id]) when an
actual transition occurs; additionally, make disconnect return early without
logging if the user was never connected (no-op path) to avoid spurious
disconnect events.
- Around line 25-28: UserPresence currently uses plain str for user IDs in its
storage and public method signatures; update it to use NotBlankStr from
core.types everywhere: import NotBlankStr, change the _counts typing to
dict[NotBlankStr, int], update method signatures for connect(user_id:
NotBlankStr), disconnect(user_id: NotBlankStr), any other methods that
accept/return user IDs (e.g., get_counts / all public APIs) to use NotBlankStr
in parameters and return types, and adjust any internal annotations/usages
accordingly so the presence API consistently enforces NotBlankStr.
In `@src/synthorg/api/auth/session.py`:
- Around line 23-25: The Session model's user_agent field is not enforcing the
documented 512-char cap; update the code that builds or defines Session (e.g.,
the Session dataclass/ORM model or the constructor used in
create_session/Session.create) to enforce a maximum length of 512 characters by
either adding a field constraint/validator or truncating the incoming user_agent
value before constructing the Session instance (ensure the symbol names
user_agent and Session are targeted so all session creation paths abide by the
cap).
In `@src/synthorg/api/state.py`:
- Around line 467-475: Replace the hard-coded "app.state.session_store_set"
string with dedicated event constants: import the appropriate event names (e.g.,
SESSION_STORE_ALREADY_CONFIGURED and SESSION_STORE_CONFIGURED) from the
synthorg.observability.events module used by the repo and use them in
logger.warning and logger.info respectively inside set_session_store();
specifically, update the logger.warning call that signals the
failure/already-configured path to use the SESSION_STORE_ALREADY_CONFIGURED
constant and update the logger.info success call to use
SESSION_STORE_CONFIGURED, and add the new imports at the top of the file.
In `@src/synthorg/persistence/sqlite/settings_repo.py`:
- Around line 124-133: The optimistic-CAS branch that checks expected_updated_at
only issues an UPDATE, so when expected_updated_at is the empty-string sentinel
(meaning “no DB row yet”) the first edit fails; modify the method handling
expected_updated_at to detect the "no-row" sentinel and perform an
insert-if-absent path: after the UPDATE returns zero rows, if
expected_updated_at == "" then attempt to INSERT the (namespace, key, value,
updated_at) row (or use a single SQL upsert/INSERT OR IGNORE then UPDATE) and
treat a successful insert as CAS success; reference the symbols
expected_updated_at, updated_at, namespace, key, value, cursor and self._db in
settings_repo.py to locate where to add the insert/UPSERT branch.
🪄 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: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1db3b5ac-0647-4ab8-b5f2-c9cd735171d2
📒 Files selected for processing (44)
CLAUDE.mddocs/design/operations.mddocs/design/page-structure.mddocs/errors.mdsrc/synthorg/api/app.pysrc/synthorg/api/auth/__init__.pysrc/synthorg/api/auth/controller.pysrc/synthorg/api/auth/middleware.pysrc/synthorg/api/auth/presence.pysrc/synthorg/api/auth/service.pysrc/synthorg/api/auth/session.pysrc/synthorg/api/auth/session_store.pysrc/synthorg/api/channels.pysrc/synthorg/api/concurrency.pysrc/synthorg/api/config.pysrc/synthorg/api/controllers/settings.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/api/errors.pysrc/synthorg/api/lifecycle.pysrc/synthorg/api/server.pysrc/synthorg/api/state.pysrc/synthorg/observability/events/api.pysrc/synthorg/persistence/protocol.pysrc/synthorg/persistence/repositories.pysrc/synthorg/persistence/sqlite/backend.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/service.pytests/unit/api/auth/test_controller.pytests/unit/api/auth/test_middleware.pytests/unit/api/auth/test_presence.pytests/unit/api/auth/test_service.pytests/unit/api/auth/test_session_store.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/fakes.pytests/unit/api/test_channels.pytests/unit/api/test_concurrency.pytests/unit/api/test_config.pytests/unit/api/test_errors.pytests/unit/api/test_server.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/persistence/test_protocol.py
| if scope not in _VALID_SCOPES: | ||
| msg = f"Invalid scope: {scope!r}. Valid: own, all" | ||
| raise ApiValidationError(msg) | ||
|
|
||
| app_state = request.app.state["app_state"] | ||
| store = app_state.session_store | ||
|
|
||
| if scope == "all" and auth_user.role == HumanRole.CEO: | ||
| sessions = await store.list_all() | ||
| else: | ||
| sessions = await store.list_by_user( | ||
| auth_user.user_id, | ||
| ) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Non-CEO scope=all silently falls back to own sessions.
When a non-CEO requests scope=all, the code falls back to list_by_user() without any indication. While this is a safe fallback, it may surprise API consumers who expect an error or explicit acknowledgment that their scope was reduced.
Consider either:
- Returning 403 for non-CEO requesting
scope=all, or - Adding a response header (e.g.,
X-Scope-Applied: own) to indicate the actual scope used
This is a design consideration rather than a defect—the current behavior prevents information leakage about other users' sessions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/controller.py` around lines 734 - 746, The current
handler silently falls back to list_by_user when a non-CEO requests scope="all";
update the branch after the _VALID_SCOPES check to make the behavior explicit:
either (A) enforce denial by raising a 403 (e.g., raise ApiForbiddenError or
similar) when scope == "all" and auth_user.role != HumanRole.CEO so only CEOs
can use store.list_all, or (B) keep the safe fallback but set an explicit
response header (e.g., response.headers["X-Scope-Applied"] = "own") before
calling store.list_by_user to indicate the reduced scope; implement the chosen
change in the controller where auth_user, _VALID_SCOPES, HumanRole.CEO,
store.list_all and store.list_by_user are referenced (i.e., immediately before
calling store.list_by_user).
| def __init__(self, db: aiosqlite.Connection) -> None: | ||
| self._db = db | ||
| self._revoked: set[str] = set() |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use NotBlankStr on the store’s public ID parameters.
NotBlankStr is already imported and used when rows come back from SQLite, but the public entry points still accept plain str for session_id / user_id. That leaves the store boundary inconsistent with the rest of the auth layer.
As per coding guidelines: "Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators."
Also applies to: 104-108, 120-123, 159-163, 183-193, 222-228
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/session_store.py` around lines 54 - 56, The public
methods on the session store accept plain str for identifier parameters; update
all public API parameter types to use NotBlankStr (e.g., change session_id: str
and user_id: str to session_id: NotBlankStr and user_id: NotBlankStr) in the
SessionStore class methods referenced (including create, get/fetch,
revoke/is_revoked and any methods accepting tuples/optionals of IDs at the noted
locations), and adjust any tuple/optional type annotations to the corresponding
NotBlankStr variants; keep using the existing imported NotBlankStr and ensure
internal uses still treat values as strings where needed.
| logger.debug( | ||
| API_SESSION_CREATED, | ||
| session_id=session.session_id, | ||
| user_id=session.user_id, | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Promote session lifecycle logs to INFO.
Session creation and cleanup are state transitions, but these paths still emit at DEBUG. That makes them inconsistent with revocation and easy to miss in normal operations.
♻️ Suggested change
- logger.debug(
+ logger.info(
API_SESSION_CREATED,
session_id=session.session_id,
user_id=session.user_id,
)
@@
- logger.debug(API_SESSION_CLEANUP, removed=len(ids))
+ logger.info(API_SESSION_CLEANUP, removed=len(ids))As per coding guidelines: "All state transitions must log at INFO level."
Also applies to: 261-261
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/auth/session_store.py` around lines 98 - 102, The session
lifecycle logs are using logger.debug for state transitions; change the calls
that log API_SESSION_CREATED (in function handling session creation where
logger.debug(API_SESSION_CREATED, session_id=session.session_id,
user_id=session.user_id)) and the corresponding session cleanup log (the
logger.debug that emits the cleanup constant, e.g., API_SESSION_CLEANED_UP) to
logger.info while preserving the same message key and structured fields
(session_id, user_id) so these transitions emit at INFO level to comply with the
guidelines.
| def _normalize_empty_tls(cls, data: dict[str, object]) -> dict[str, object]: | ||
| """Normalize empty-string TLS paths to ``None``.""" | ||
| if isinstance(data, dict): | ||
| for key in ("ssl_certfile", "ssl_keyfile", "ssl_ca_certs"): | ||
| val = data.get(key) | ||
| if isinstance(val, str) and not val.strip(): | ||
| data[key] = None | ||
| return data |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Don’t mutate the raw config mapping in the before validator.
_normalize_empty_tls() rewrites the incoming data dict in place. If the same parsed config object is reused elsewhere, model construction now changes caller-owned state as a side effect. Normalize a copied mapping and return that instead.
As per coding guidelines, “Create new objects for immutability, never mutate existing ones; use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/config.py` around lines 182 - 189, The before-validator
_normalize_empty_tls currently mutates the incoming data mapping in place;
instead make and operate on a copy and return that to avoid side effects. Update
_normalize_empty_tls to deep-copy the incoming dict (use copy.deepcopy) into a
new variable, perform the empty-string-to-None normalization on that copy for
keys ("ssl_certfile", "ssl_keyfile", "ssl_ca_certs"), and return the copy; also
ensure callers ultimately receive an immutable mapping by wrapping the final
constructed mapping with MappingProxyType as per guidelines.
| @pytest.fixture | ||
| async def db() -> AsyncGenerator[aiosqlite.Connection]: |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
AsyncGenerator should have two type parameters.
AsyncGenerator[T] is shorthand for AsyncGenerator[T, None] in recent Python, but for clarity and strict mypy compliance, consider the full form.
♻️ Suggested fix
`@pytest.fixture`
-async def db() -> AsyncGenerator[aiosqlite.Connection]:
+async def db() -> AsyncGenerator[aiosqlite.Connection, None]:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @pytest.fixture | |
| async def db() -> AsyncGenerator[aiosqlite.Connection]: | |
| `@pytest.fixture` | |
| async def db() -> AsyncGenerator[aiosqlite.Connection, None]: |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/unit/api/auth/test_session_store.py` around lines 65 - 66, The db
fixture's return annotation uses AsyncGenerator with a single type parameter;
update the type hint on the fixture function db to use the full form
AsyncGenerator[aiosqlite.Connection, None] (i.e., include the second type
parameter None) so static type checkers like mypy accept it; change the
annotation on the async def db(...) to AsyncGenerator[aiosqlite.Connection,
None] and adjust imports if necessary.
| @pytest.mark.unit | ||
| class TestUserChannelHelpers: | ||
| def test_user_channel_returns_prefixed(self) -> None: | ||
| assert user_channel("abc") == "user:abc" | ||
|
|
||
| def test_is_user_channel_true(self) -> None: | ||
| assert is_user_channel("user:abc") is True | ||
|
|
||
| def test_is_user_channel_false(self) -> None: | ||
| assert is_user_channel("tasks") is False | ||
|
|
||
| def test_extract_user_id_valid(self) -> None: | ||
| assert extract_user_id("user:abc") == "abc" | ||
|
|
||
| def test_extract_user_id_non_user_channel(self) -> None: | ||
| assert extract_user_id("tasks") is None | ||
|
|
||
| def test_extract_user_id_empty_suffix(self) -> None: | ||
| assert extract_user_id("user:") == "" |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Test documents a gap in validation.
The test at lines 72-73 (extract_user_id("user:") == "") documents the current behavior where an empty suffix is accepted. When the validation fix for empty user IDs is applied to channels.py, this test should be updated to expect a ValueError or None for malformed channel names.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/unit/api/test_channels.py` around lines 55 - 73, The test currently
accepts an empty user suffix for extract_user_id
(test_extract_user_id_empty_suffix), but after adding validation in channels.py
extract_user_id should reject "user:"; update the test to assert the new
behavior—either expect a ValueError to be raised when calling
extract_user_id("user:") or assert it returns None if you choose to signal
invalid channels that way—adjust test_extract_user_id_empty_suffix accordingly
and keep other tests (user_channel, is_user_channel,
test_extract_user_id_valid/non_user_channel) unchanged; reference
extract_user_id, user_channel, and is_user_channel when making the change.
- revoke_all_for_user: filter by expires_at in UPDATE (skip expired sessions) - _handle_subscribe: use _channel_allowed to filter budget channels for non-CEO/Manager users (was accepting subscribe then silently not delivering) - Add SETTINGS_VERSION_CONFLICT event constant (was reusing VALIDATION_FAILED) - settings_repo CAS: handle empty-string sentinel (first write to unset key) - Fix AsyncGenerator type hint in test fixture
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/synthorg/settings/service.py (1)
481-494: 🧹 Nitpick | 🔵 TrivialDocument the first-write CAS sentinel.
_check_setting_etag()now passescurrent.updated_at or ""for the “no DB override yet” case, but this public contract only describes matching an existingupdated_at. Please document""as the “expect no stored override” token—or replace it with an explicit sentinel type—so other callers can safely use the optimistic-concurrency path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/settings/service.py` around lines 481 - 494, Update the docstring for the update method to document the first-write CAS sentinel: explain that passing expected_updated_at == "" (or conceptually an explicit sentinel) means “expect no stored override yet” and will only succeed if the DB has no updated_at present; reference the _check_setting_etag helper and the expected_updated_at parameter so callers know they can use the empty-string optimistic-concurrency token (or alternatively switch to/mention an explicit sentinel type) when they intend first-write semantics.
♻️ Duplicate comments (1)
src/synthorg/api/controllers/ws.py (1)
498-508: 🧹 Nitpick | 🔵 TrivialUse
API_WS_TRANSPORT_ERRORfor unsubscribe failures.The event constant
API_WS_DISCONNECTEDis used for the unsubscribe error log, but this is semantically incorrect—it represents a transport/cleanup error, not the disconnection itself (which is logged separately on line 509). This could confuse log analysis and monitoring.Suggested fix
except Exception: - logger.error( + logger.warning( - API_WS_DISCONNECTED, - error="Failed to unsubscribe", + API_WS_TRANSPORT_ERROR, + reason="unsubscribe_failed", + user_id=user.user_id, client=str(socket.client), exc_info=True, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/controllers/ws.py` around lines 498 - 508, The unsubscribe exception logs use the wrong event constant: replace API_WS_DISCONNECTED with API_WS_TRANSPORT_ERROR in the logger.error call inside the try/except around channels_plugin.unsubscribe(subscriber) so transport/cleanup failures are correctly categorized; update the logger.error invocation (the one that also passes error="Failed to unsubscribe", client=str(socket.client), exc_info=True) to use API_WS_TRANSPORT_ERROR instead of API_WS_DISCONNECTED to keep disconnection and transport errors distinct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/controllers/ws.py`:
- Around line 416-420: The code redundantly calls user_channel(user.user_id) in
both _setup_connection and ws_handler; modify _setup_connection to compute and
return the user's private channel string (alongside or as part of its current
return payload), update ws_handler to consume that returned user channel instead
of calling user_channel(...) again, and remove the duplicate user_channel call;
ensure references to the returned value (from _setup_connection) are used where
all_subs is built (and update any other callers of _setup_connection to handle
the new return shape).
---
Outside diff comments:
In `@src/synthorg/settings/service.py`:
- Around line 481-494: Update the docstring for the update method to document
the first-write CAS sentinel: explain that passing expected_updated_at == "" (or
conceptually an explicit sentinel) means “expect no stored override yet” and
will only succeed if the DB has no updated_at present; reference the
_check_setting_etag helper and the expected_updated_at parameter so callers know
they can use the empty-string optimistic-concurrency token (or alternatively
switch to/mention an explicit sentinel type) when they intend first-write
semantics.
---
Duplicate comments:
In `@src/synthorg/api/controllers/ws.py`:
- Around line 498-508: The unsubscribe exception logs use the wrong event
constant: replace API_WS_DISCONNECTED with API_WS_TRANSPORT_ERROR in the
logger.error call inside the try/except around
channels_plugin.unsubscribe(subscriber) so transport/cleanup failures are
correctly categorized; update the logger.error invocation (the one that also
passes error="Failed to unsubscribe", client=str(socket.client), exc_info=True)
to use API_WS_TRANSPORT_ERROR instead of API_WS_DISCONNECTED to keep
disconnection and transport errors distinct.
🪄 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: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 61199285-1f2a-44a0-aa52-80ba255bc4fa
📒 Files selected for processing (5)
src/synthorg/api/auth/session_store.pysrc/synthorg/api/controllers/ws.pysrc/synthorg/observability/events/settings.pysrc/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/settings/service.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Nofrom __future__ import annotations-- Python 3.14 has PEP 649
Useexcept A, B:(no parentheses) for PEP 758 except syntax on Python 3.14
All public functions and classes must have type hints; mypy strict mode applies
Docstrings must use Google style and are required on all public classes and functions (enforced by ruff D rules)
Create new objects for immutability, never mutate existing ones; use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement
Use frozen Pydantic models for config/identity; separate mutable-via-copy models for runtime state that evolves
Use Pydantic v2 with allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields
Use@computed_fieldfor derived values instead of storing and validating redundant fields (e.g., TokenUsage.total_tokens)
Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g., multiple tool invocations, parallel agent calls)
Functions must be less than 50 lines; files must be less than 800 lines
Handle errors explicitly, never silently swallow exceptions
Validate at system boundaries: user input, external APIs, and config files
Every module with business logic must import get_logger from synthorg.observability and initialize aslogger = get_logger(__name__)
Never use import logging, logging.getLogger(), or print() in application code (exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py)
Always useloggeras the variable name (not_loggerorlog)
Always use structured logging with constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api), never use string formatting (logger.info(EVENT, key=value))
All error paths must log at WARNING or ERROR with context before ...
Files:
src/synthorg/observability/events/settings.pysrc/synthorg/settings/service.pysrc/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/api/auth/session_store.pysrc/synthorg/api/controllers/ws.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/observability/events/settings.pysrc/synthorg/settings/service.pysrc/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/api/auth/session_store.pysrc/synthorg/api/controllers/ws.py
🧠 Learnings (29)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/observability/events/settings.pysrc/synthorg/settings/service.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)
Applied to files:
src/synthorg/observability/events/settings.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Applied to files:
src/synthorg/observability/events/settings.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/settings_repo.pysrc/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `NotBlankStr` from `core.types` for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-04-03T11:47:49.775Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T11:47:49.775Z
Learning: Applies to src/**/*.py : Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to **/*.py : Use `NotBlankStr` (from `core.types`) for all identifier/name fields, including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants, instead of manual whitespace validators
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-17T11:41:02.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T11:41:02.964Z
Learning: Applies to src/**/*.py : Models: Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `computed_field` for derived values instead of storing + validating redundant fields. Use `NotBlankStr` for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to **/*.py : Models: Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. Use `computed_field` for derived values instead of storing redundant fields. Use `NotBlankStr` for all identifier/name fields.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to src/**/*.py : All state transitions must log at INFO level
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : All state transitions must log at INFO.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : All state transitions must log at INFO level
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions. Pure data models, enums, and re-exports do NOT need logging.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : Use DEBUG logging for object creation, internal flow, and entry/exit of key functions.
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Use structured logging: always `logger.info(EVENT, key=value)` — never `logger.info("msg %s", val)`
Applied to files:
src/synthorg/api/auth/session_store.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
src/synthorg/api/controllers/ws.py
🔇 Additional comments (22)
src/synthorg/api/controllers/ws.py (7)
30-36: LGTM!Clean import of channel utilities needed for the new authorization logic. The grouped import from
synthorg.api.channelsfollows project conventions.
271-286: LGTM!The
_channel_allowed()function correctly implements server-side access control:
- User channels are restricted to the owning user via
extract_user_id()comparison- Budget channels are gated to CEO/MANAGER roles
- All other channels default to allowed for any read-capable user
The logic aligns with the relevant code snippets showing
BUDGET_CHANNELSis a frozenset andextract_user_id()properly parses theuser:prefix.
327-328: LGTM!Adding the
_channel_allowed()check in_on_event()provides defense-in-depth. Events are filtered before forwarding, ensuring that even if a channel subscription leaked through, unauthorized events would still be blocked.
432-434: LGTM!Presence tracking is correctly wired into the connection setup. The
connect()call happens after successful subscription, ensuring presence is only tracked for fully established connections.
507-508: LGTM!Presence disconnect now runs unconditionally after the try/except block, ensuring it executes whether or not unsubscribe succeeds. This correctly addresses the previous review concern about sticky presence state.
651-665: LGTM!The subscription handler now correctly enforces
_channel_allowed()before acknowledging channels, addressing the previous review concern. The logic ensures:
- User's own private channel is always allowed
- Shared channels require both membership in
_ALL_CHANNELS_SETand passing authorization- Other users' private channels are silently dropped with a redacted warning log
This prevents acknowledging channels the connection can never receive.
480-487: LGTM!The
conn_useris correctly propagated through the event callback closure to_on_event(), enabling per-connection authorization checks during event forwarding.src/synthorg/api/auth/session_store.py (12)
98-102: Session creation should log at INFO level.Session creation is a state transition. As per coding guidelines: "All state transitions must log at INFO level."
120-141: UseNotBlankStrforuser_idparameter.The
user_idparameter accepts plainstrbut should useNotBlankStrfor consistency with the rest of the auth layer.As per coding guidelines: "Use NotBlankStr from core.types for all identifier/name fields."
159-181: LGTM for logic, but useNotBlankStrforsession_idparameter.The revocation logic is correct—idempotent UPDATE with proper in-memory set synchronization and INFO-level logging for the state transition.
The
session_idparameter should useNotBlankStrper coding guidelines.
235-262: Session cleanup should log at INFO level.Session cleanup is a state transition. The log at Line 261 should be promoted from
DEBUGtoINFO.As per coding guidelines: "All state transitions must log at INFO level."
1-22: LGTM!Module docstring clearly explains the hybrid design rationale. Imports are well-organized and follow project conventions.
25-40: LGTM!Properly deserializes all session fields using
NotBlankStrfor identifiers and correct type conversions for datetime and boolean fields.
43-56: LGTM!Class docstring clearly explains the hybrid design with sync O(1) lookups for the auth hot path. Constructor correctly initializes the revocation set.
58-71: LGTM!Correctly loads only non-expired revoked sessions at startup. The expiry filter is appropriate since the JWT decoder already rejects expired tokens.
104-118:get()returns revoked sessions, but AI summary claims otherwise.The query at Line 114 is
SELECT * FROM sessions WHERE session_id = ?with norevoked = 0filter, meaning this method can return revoked sessions. However, the AI summary states "get(session_id)fetches a non-revoked session."If intentional (e.g., for admin inspection), clarify in the docstring. If not, add the filter.
143-157: LGTM!Query correctly filters for active, non-expired sessions with appropriate ordering for admin views.
183-220: LGTM for logic!The TOCTOU concern from earlier review has been addressed—the UPDATE at Line 201 now includes
expires_at > ?to only revoke active sessions. The two-step UPDATE-then-SELECT approach is necessary since SQLite UPDATE doesn't return affected row IDs.The
user_idparameter should useNotBlankStrper coding guidelines (see earlier comment).
222-233: LGTM for implementation!Sync O(1) set lookup is appropriate for the auth middleware hot path. Set membership testing is atomic under Python's GIL in the asyncio single-threaded model.
The
session_idparameter should useNotBlankStrper coding guidelines (see earlier comment).src/synthorg/observability/events/settings.py (1)
17-17: Nice observability split.A dedicated
SETTINGS_VERSION_CONFLICTevent keeps concurrent-write conflicts separate from validation failures in logs and metrics.src/synthorg/persistence/sqlite/settings_repo.py (1)
124-145: Good CAS handling for the first persisted override.The
expected_updated_at == ""fallback closes the false-conflict case for settings that still resolve from env, YAML, or code defaults.src/synthorg/settings/service.py (1)
540-552: Keep transport errors out of the settings service.Raising
synthorg.api.errors.VersionConflictErrorhere reverses the dependency direction (settings → api) and couples this module to HTTP exception definitions. The concern about error handling is valid: unlessVersionConflictErroris explicitly registered in Litestar's exception handlers, the unhandled exception will default to a 500 response rather than the intended 409. Prefer creating a settings-layer conflict error (e.g.,SettingsVersionConflictError) and translating it to 409 at the API boundary in the handler layer.
| # Subscribe to all shared channels + the user's private channel. | ||
| user_ch = user_channel(user.user_id) | ||
| all_subs = [*ALL_CHANNELS, user_ch] | ||
| try: | ||
| subscriber = await channels_plugin.subscribe(list(ALL_CHANNELS)) | ||
| subscriber = await channels_plugin.subscribe(all_subs) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor redundancy: user_channel() called twice for the same user.
The user's private channel string is computed in both _setup_connection (line 417) and ws_handler (line 476). Consider returning it from _setup_connection to avoid the duplicate call.
Optional consolidation
async def _setup_connection(
socket: WebSocket[Any, Any, Any],
user: AuthenticatedUser,
*,
already_accepted: bool,
-) -> tuple[ChannelsPlugin, Any] | None:
+) -> tuple[ChannelsPlugin, Any, str] | None:
"""Resolve plugin, accept the connection, and subscribe to channels.
- Returns ``(channels_plugin, subscriber)`` on success, or ``None``
+ Returns ``(channels_plugin, subscriber, user_channel)`` on success, or ``None``
(socket already closed) on failure.
...
"""
...
- return channels_plugin, subscriber
+ return channels_plugin, subscriber, user_chThen in ws_handler:
- channels_plugin, subscriber = setup
+ channels_plugin, subscriber, user_ch = setup
- # Auto-subscribe to the user's private channel.
- user_ch = user_channel(user.user_id)
+ # Auto-subscribe to the user's private channel (returned from setup).
subscribed: set[str] = {user_ch}Also applies to: 475-477
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/controllers/ws.py` around lines 416 - 420, The code
redundantly calls user_channel(user.user_id) in both _setup_connection and
ws_handler; modify _setup_connection to compute and return the user's private
channel string (alongside or as part of its current return payload), update
ws_handler to consume that returned user channel instead of calling
user_channel(...) again, and remove the duplicate user_channel call; ensure
references to the returned value (from _setup_connection) are used where
all_subs is built (and update any other callers of _setup_connection to handle
the new return shape).
… sentinel - Unsubscribe cleanup error now uses API_WS_TRANSPORT_ERROR (was conflating with API_WS_DISCONNECTED) - Document empty-string first-write CAS sentinel in service docstring
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/settings/service.py`:
- Around line 544-546: The settings service currently imports
VersionConflictError from synthorg.api.errors, creating an inverted dependency;
fix by moving the domain error into a shared location (e.g., create
VersionConflictError or a base ConflictError in synthorg.core.errors or
synthorg.errors) and update references in settings/service.py to import that
shared error (or alternatively add a SettingsConflictError in
synthorg.settings.errors and have the API layer catch/translate it to
VersionConflictError); update import statements in settings/service.py (where
VersionConflictError is referenced) and in synthorg.api.errors to re-export or
subclass the shared error so no service module imports from the api layer.
🪄 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: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 74c5b727-8255-4d67-a1b2-96e277451ae4
📒 Files selected for processing (2)
src/synthorg/api/controllers/ws.pysrc/synthorg/settings/service.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Deploy Preview
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Nofrom __future__ import annotations-- Python 3.14 has PEP 649
Useexcept A, B:(no parentheses) for PEP 758 except syntax on Python 3.14
All public functions and classes must have type hints; mypy strict mode applies
Docstrings must use Google style and are required on all public classes and functions (enforced by ruff D rules)
Create new objects for immutability, never mutate existing ones; use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement
Use frozen Pydantic models for config/identity; separate mutable-via-copy models for runtime state that evolves
Use Pydantic v2 with allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields
Use@computed_fieldfor derived values instead of storing and validating redundant fields (e.g., TokenUsage.total_tokens)
Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g., multiple tool invocations, parallel agent calls)
Functions must be less than 50 lines; files must be less than 800 lines
Handle errors explicitly, never silently swallow exceptions
Validate at system boundaries: user input, external APIs, and config files
Every module with business logic must import get_logger from synthorg.observability and initialize aslogger = get_logger(__name__)
Never use import logging, logging.getLogger(), or print() in application code (exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py)
Always useloggeras the variable name (not_loggerorlog)
Always use structured logging with constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api), never use string formatting (logger.info(EVENT, key=value))
All error paths must log at WARNING or ERROR with context before ...
Files:
src/synthorg/settings/service.pysrc/synthorg/api/controllers/ws.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/settings/service.pysrc/synthorg/api/controllers/ws.py
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/settings/service.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Applied to files:
src/synthorg/settings/service.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
src/synthorg/api/controllers/ws.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
src/synthorg/api/controllers/ws.py
🔇 Additional comments (11)
src/synthorg/settings/service.py (4)
27-27: LGTM!The dedicated
SETTINGS_VERSION_CONFLICTevent constant provides clear observability signals for concurrent modification patterns, properly distinguishing version conflicts from validation failures.
467-498: LGTM!The method signature properly adds
expected_updated_atas a keyword-only parameter with clear typing. The docstring comprehensively documents the CAS semantics, including the empty-string sentinel for first-write scenarios and the newVersionConflictErrorexception.
548-556: LGTM!The conflict handling correctly logs the version mismatch with structured fields including the expected timestamp, then raises with an informative message. The
SETTINGS_VERSION_CONFLICTevent provides clear observability for concurrent modification scenarios.
658-660: LGTM!The PEP 758 except syntax and system-error propagation pattern are compliant with project conventions. Notification failures are correctly logged at WARNING level without breaking the settings write operation.
src/synthorg/api/controllers/ws.py (7)
416-420: Minor redundancy:user_channel()called twice for the same user.The user's private channel string is computed at line 417 in
_setup_connectionand again at line 476 inws_handler. Consider returning it from_setup_connectionto eliminate the duplicate call.Also applies to: 475-477
271-286: LGTM!The
_channel_allowed()function provides clean server-side access control with clear separation of concerns: user channel ownership check, budget channel role restriction, and permissive default for shared channels. The pure function design with no side effects is appropriate here.
327-328: LGTM!The
_channel_allowedcheck is correctly placed after the subscription check but before filter evaluation, providing defense-in-depth. This ensures events are never forwarded to unauthorized users even if the subscription state is somehow inconsistent.
497-509: LGTM!The teardown correctly places
user_presence.disconnect()in the outerfinallyblock, ensuring it runs unconditionally even if unsubscribe fails. The nested try/except properly isolates unsubscribe errors. This addresses the previous review concern about sticky presence.
532-537: LGTM!Adding
user_idto the error log context improves debuggability when transport errors occur.
651-665: LGTM!The subscription authorization logic correctly:
- Always allows the user's own private channel
- Validates shared channels against
_channel_allowed()before adding to subscriptions- Logs (with redaction) and silently drops attempts to subscribe to other users' channels
This properly addresses the previous concern about acknowledging channels the connection can never receive.
432-434: Remove the defensive try/except suggestion —user_presence.connect()cannot realistically raise.The
connect()method performs only non-raising operations: dict lookup, integer addition, and dict assignment. There is no realistic exception path that would leak the subscriber, making the suggested defensive pattern unnecessary complexity.> Likely an incorrect or invalid review comment.
| from synthorg.api.errors import ( # noqa: PLC0415 | ||
| VersionConflictError, | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Inverted layer dependency: service imports from API layer.
The settings service (settings/) imports VersionConflictError from synthorg.api.errors, creating an inverted dependency where a lower-level service module depends on the presentation layer. This violates the Dependency Inversion Principle and risks circular imports.
Consider defining VersionConflictError (or a base conflict error) in a shared location like synthorg.core.errors or synthorg.errors, then having api.errors re-export or extend it. Alternatively, define a domain-specific SettingsConflictError in the settings package and let the API layer catch and convert it.
♻️ Proposed refactor sketch
Option 1: Move to shared errors module (e.g., synthorg.core.errors):
# In synthorg/core/errors.py
class VersionConflictError(Exception):
"""Raised when a CAS version check fails."""
...Then import from synthorg.core.errors in both settings/service.py and api/errors.py.
Option 2: Define a domain error in settings, convert in API:
# In synthorg/settings/errors.py
class SettingsConflictError(SettingsError):
"""Raised when a concurrent modification is detected."""
...Then in the API controller, catch SettingsConflictError and raise VersionConflictError.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/settings/service.py` around lines 544 - 546, The settings
service currently imports VersionConflictError from synthorg.api.errors,
creating an inverted dependency; fix by moving the domain error into a shared
location (e.g., create VersionConflictError or a base ConflictError in
synthorg.core.errors or synthorg.errors) and update references in
settings/service.py to import that shared error (or alternatively add a
SettingsConflictError in synthorg.settings.errors and have the API layer
catch/translate it to VersionConflictError); update import statements in
settings/service.py (where VersionConflictError is referenced) and in
synthorg.api.errors to re-export or subclass the shared error so no service
module imports from the api layer.
🤖 I have created a release *beep* *boop* --- ## [0.5.9](v0.5.8...v0.5.9) (2026-04-03) ### Features * ceremony template defaults + strategy migration UX ([#1031](#1031)) ([da4a8e1](da4a8e1)), closes [#976](#976) [#978](#978) * hybrid search (dense + BM25 sparse) for memory retrieval pipeline ([#1016](#1016)) ([fccac4a](fccac4a)), closes [#694](#694) * implement network hosting and multi-user access ([#1032](#1032)) ([398c378](398c378)), closes [#244](#244) * implement visual workflow editor ([#247](#247)) ([#1018](#1018)) ([ef5d3c1](ef5d3c1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Implements network hosting and multi-user access for SynthOrg, transitioning from local-first to network-ready with production-grade session management, concurrent access handling, and per-user WebSocket isolation.
Changes
Network Exposure Configuration
ServerConfig(ssl_certfile,ssl_keyfile,ssl_ca_certs) with pairwise validationtrusted_proxies) forX-Forwarded-For/X-Forwarded-Protobehind reverse proxies0.0.0.0without TLS or trusted proxy configurationrestart_required=True)Session Management
Sessionmodel +SessionStore(hybrid in-memory revocation set + SQLite persistence)jticlaim for per-token session tracking and revocationGET /auth/sessions(own sessions; CEO?scope=all),DELETE /auth/sessions/{id},POST /auth/logoutload_revoked()at startupConcurrent Access Handling
compute_etag()(SHA-256 weak ETag) +check_if_match()utilitiesVersionConflictErroron mismatch, backward compatible (noIf-Match= bypass)Multi-User WebSocket
user:{id}channels with server-side isolation (_channel_allowed)UserPresencetracker (in-memory, multi-tab aware)New Error Codes
SESSION_REVOKED(1002) -- revoked session detected in middlewareVERSION_CONFLICT(4002) -- ETag/If-Match mismatchPre-PR Review
Pre-reviewed by 8 agents (code-reviewer, security-reviewer, silent-failure-hunter, test-analyzer, conventions-enforcer, async-reviewer, docs-checker, issue-verifier). 18 findings addressed:
_create_sessionin try/except (was crashing login on DB error)UserPresenceintoAppState(was dead code)_extract_jtiexception handling + added loggingrevoke_sessionto prevent session ID enumerationrevoke_all_for_user(UPDATE first, then re-query)user_id/usernametoSessionResponsefor CEO admin view_check_setting_etaghelper (function under 50 lines)Test Plan
test_session_store.py(16 tests),test_presence.py(6 tests),test_concurrency.py(9 tests)create_token()triple return typeVisual Testing
uv run python -m pytest tests/ -m unit -n 8 -k authnpm --prefix web run dev)Closes #244