Skip to content

feat: implement network hosting and multi-user access#1032

Merged
Aureliolo merged 8 commits intomainfrom
feat/network-hosting-multi-user
Apr 3, 2026
Merged

feat: implement network hosting and multi-user access#1032
Aureliolo merged 8 commits intomainfrom
feat/network-hosting-multi-user

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

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

  • TLS fields on ServerConfig (ssl_certfile, ssl_keyfile, ssl_ca_certs) with pairwise validation
  • Trusted proxy support (trusted_proxies) for X-Forwarded-For/X-Forwarded-Proto behind reverse proxies
  • Warning when binding to 0.0.0.0 without TLS or trusted proxy configuration
  • 4 new API settings (all restart_required=True)

Session Management

  • Session model + SessionStore (hybrid in-memory revocation set + SQLite persistence)
  • JWT jti claim for per-token session tracking and revocation
  • GET /auth/sessions (own sessions; CEO ?scope=all), DELETE /auth/sessions/{id}, POST /auth/logout
  • Sync O(1) revocation check in auth middleware hot path
  • Session creation on login/setup (best-effort, non-blocking)
  • Background cleanup for expired sessions
  • Revocations survive server restarts via load_revoked() at startup

Concurrent Access Handling

  • ETag/If-Match optimistic concurrency on settings endpoints
  • compute_etag() (SHA-256 weak ETag) + check_if_match() utilities
  • 409 VersionConflictError on mismatch, backward compatible (no If-Match = bypass)

Multi-User WebSocket

  • Dynamic user:{id} channels with server-side isolation (_channel_allowed)
  • Role-based event filtering (budget events restricted to CEO/Manager)
  • UserPresence tracker (in-memory, multi-tab aware)
  • Presence wired into WS connect/disconnect lifecycle

New Error Codes

  • SESSION_REVOKED (1002) -- revoked session detected in middleware
  • VERSION_CONFLICT (4002) -- ETag/If-Match mismatch

Pre-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:

  • Wrapped _create_session in try/except (was crashing login on DB error)
  • Wired UserPresence into AppState (was dead code)
  • Fixed session cleanup comment/logic mismatch and wrong event constant
  • Narrowed _extract_jti exception handling + added logging
  • Merged 404/403 in revoke_session to prevent session ID enumeration
  • Fixed TOCTOU in revoke_all_for_user (UPDATE first, then re-query)
  • Added user_id/username to SessionResponse for CEO admin view
  • Extracted _check_setting_etag helper (function under 50 lines)
  • Removed 3 unused event constants
  • Updated CLAUDE.md, operations.md, errors.md, page-structure.md

Test Plan

  • 12,807 tests pass (0 failures)
  • New test files: test_session_store.py (16 tests), test_presence.py (6 tests), test_concurrency.py (9 tests)
  • Existing tests updated for create_token() triple return type
  • All checks green: ruff, mypy, pytest

Visual Testing

  • After TLS config: uv run python -m pytest tests/ -m unit -n 8 -k auth
  • After session management: test login flow in dashboard (npm --prefix web run dev)
  • After concurrent access: verify ETag with multiple browser tabs editing settings

Closes #244

Copilot AI review requested due to automatic review settings April 3, 2026 10:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Walkthrough

Adds server-side JWT sessions using per-token jti values: an immutable Session model, an async SQLite-backed SessionStore with an in-memory revoked-ID set, session lifecycle endpoints (GET /auth/sessions, DELETE /auth/sessions/{id}, POST /auth/logout), middleware revocation checks, and periodic session cleanup. Introduces WebSocket user presence tracking and per-user channel authorization with dynamic user:{id} channels. Implements optimistic concurrency (ETag/If-Match) utilities and applies them to settings endpoints. Extends server config with TLS and trusted-proxies, updates persistence schema, observability events, error codes, and extensive tests.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement network hosting and multi-user access' clearly and concisely summarizes the main change—introducing network hosting capabilities with multi-user support—which aligns with the substantial changeset across session management, TLS configuration, concurrent access handling, and WebSocket isolation.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset. It outlines network exposure configuration, session management, concurrent access handling, multi-user WebSocket support, and new error codes—all matching the actual code changes in the raw summary.
Linked Issues check ✅ Passed The changeset meets the primary objectives from issue #244: network exposure configuration with TLS and trusted proxies [config.py, server.py, settings definitions], session management with persistence and revocation [session.py, session_store.py, controller.py, middleware.py, lifecycle.py], optimistic concurrency control with ETag/If-Match [concurrency.py, controllers/settings.py], and multi-user WebSocket channels with presence tracking [channels.py, controllers/ws.py, presence.py].
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the stated objectives from issue #244. The changes focus exclusively on network hosting, session management, concurrent access, and multi-user WebSocket support, with no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 47.89% which is sufficient. The required threshold is 40.00%.

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


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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 8ecd1b6.
Ensure 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 Files

None

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Invalid exception handling syntax. Multiple exceptions must be parenthesized in Python 3: except (MemoryError, RecursionError):.

Suggested change
except MemoryError, RecursionError:
except (MemoryError, RecursionError):

Comment on lines +162 to +183
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}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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 = ?).

Comment on lines +256 to +271
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 67.42081% with 144 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.45%. Comparing base (fccac4a) to head (8ecd1b6).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/api/auth/controller.py 28.57% 70 Missing ⚠️
src/synthorg/persistence/sqlite/settings_repo.py 23.07% 9 Missing and 1 partial ⚠️
src/synthorg/api/auth/session.py 52.63% 6 Missing and 3 partials ⚠️
src/synthorg/api/config.py 79.06% 6 Missing and 3 partials ⚠️
src/synthorg/api/controllers/settings.py 57.89% 7 Missing and 1 partial ⚠️
src/synthorg/api/app.py 0.00% 7 Missing ⚠️
src/synthorg/api/controllers/ws.py 82.35% 6 Missing ⚠️
src/synthorg/api/state.py 70.00% 6 Missing ⚠️
src/synthorg/settings/service.py 28.57% 4 Missing and 1 partial ⚠️
src/synthorg/api/lifecycle.py 55.55% 4 Missing ⚠️
... and 5 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +179 to +205
@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 "
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +182
# 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

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +220
# 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(
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +36
"""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}"'
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +443 to +458
@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

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Stale 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7639021 and c3551b2.

📒 Files selected for processing (36)
  • CLAUDE.md
  • docs/design/operations.md
  • docs/design/page-structure.md
  • docs/errors.md
  • src/synthorg/api/app.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/auth/controller.py
  • src/synthorg/api/auth/middleware.py
  • src/synthorg/api/auth/presence.py
  • src/synthorg/api/auth/service.py
  • src/synthorg/api/auth/session.py
  • src/synthorg/api/auth/session_store.py
  • src/synthorg/api/channels.py
  • src/synthorg/api/concurrency.py
  • src/synthorg/api/config.py
  • src/synthorg/api/controllers/settings.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/server.py
  • src/synthorg/api/state.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/settings/definitions/api.py
  • tests/unit/api/auth/test_middleware.py
  • tests/unit/api/auth/test_presence.py
  • tests/unit/api/auth/test_service.py
  • tests/unit/api/auth/test_session_store.py
  • tests/unit/api/conftest.py
  • tests/unit/api/controllers/test_ws.py
  • tests/unit/api/test_channels.py
  • tests/unit/api/test_concurrency.py
  • tests/unit/api/test_config.py
  • tests/unit/api/test_errors.py
  • tests/unit/api/test_server.py
  • tests/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: No from __future__ import annotations in 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), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.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 (using model_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 use allow_inf_nan=False in ConfigDict declarations to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing + validating redundant fields (e.g. TokenUsage.total_tokens)
Use NotBlankStr (from core.types) for all identifier/name fields -- including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) 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). Prefer structured concurrency over bare create_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.py
  • tests/unit/api/conftest.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • src/synthorg/api/auth/middleware.py
  • tests/unit/api/auth/test_service.py
  • tests/unit/api/auth/test_middleware.py
  • src/synthorg/api/server.py
  • src/synthorg/api/auth/__init__.py
  • tests/unit/api/test_concurrency.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/app.py
  • tests/unit/api/auth/test_presence.py
  • src/synthorg/api/auth/service.py
  • tests/unit/api/controllers/test_ws.py
  • tests/unit/api/test_config.py
  • tests/unit/api/test_server.py
  • src/synthorg/api/concurrency.py
  • src/synthorg/settings/definitions/api.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/auth/session.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/config.py
  • tests/unit/api/auth/test_session_store.py
  • tests/unit/api/test_errors.py
  • src/synthorg/api/channels.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/api/controllers/settings.py
  • src/synthorg/api/auth/controller.py
  • src/synthorg/api/auth/presence.py
  • src/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: use asyncio_mode = "auto" -- no manual @pytest.mark.asyncio needed
Test timeout: 30 seconds per test (global in pyproject.toml -- do not add per-file pytest.mark.timeout(30) markers; non-default overrides like timeout(60) are allowed)
Prefer @pytest.mark.parametrize for testing similar cases
For property-based testing in Python, use Hypothesis (@given + @settings). Profiles: ci (50 examples, default) and dev (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, mock time.monotonic() and asyncio.sleep(). For tasks that must block indefinitely, use asyncio.Event().wait() instead of asyncio.sleep(large_number).

Files:

  • tests/unit/api/test_channels.py
  • tests/unit/api/conftest.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/api/auth/test_service.py
  • tests/unit/api/auth/test_middleware.py
  • tests/unit/api/test_concurrency.py
  • tests/unit/api/auth/test_presence.py
  • tests/unit/api/controllers/test_ws.py
  • tests/unit/api/test_config.py
  • tests/unit/api/test_server.py
  • tests/unit/api/auth/test_session_store.py
  • tests/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_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code (exceptions: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr))
Logger variable name must always be logger (not _logger, not log)
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
Always use structured kwargs for logging: logger.info(EVENT, key=value) -- never logger.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, use allow_inf_nan=False in all ConfigDict declarations
Pure data models, enums, and re-exports do NOT need logging

Files:

  • src/synthorg/api/auth/middleware.py
  • src/synthorg/api/server.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/app.py
  • src/synthorg/api/auth/service.py
  • src/synthorg/api/concurrency.py
  • src/synthorg/settings/definitions/api.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/auth/session.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/config.py
  • src/synthorg/api/channels.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/api/controllers/settings.py
  • src/synthorg/api/auth/controller.py
  • src/synthorg/api/auth/presence.py
  • src/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.md
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/app.py
  • tests/unit/api/controllers/test_ws.py
  • src/synthorg/settings/definitions/api.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/channels.py
  • src/synthorg/api/controllers/settings.py
  • src/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.md
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/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.md
  • src/synthorg/api/auth/__init__.py
  • tests/unit/api/controllers/test_ws.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/controllers/settings.py
  • src/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.md
  • src/synthorg/api/app.py
  • tests/unit/api/controllers/test_ws.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/controllers/settings.py
  • src/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.md
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/state.py
  • 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:

  • CLAUDE.md
  • src/synthorg/api/auth/middleware.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/app.py
  • tests/unit/api/controllers/test_ws.py
  • src/synthorg/api/state.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/controllers/settings.py
  • src/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.md
  • src/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.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/auth/service.py
  • src/synthorg/api/state.py
  • src/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__.py
  • src/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.py
  • tests/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.py
  • src/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.py
  • src/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.py
  • src/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.py
  • src/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.py
  • src/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.py
  • src/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.py
  • src/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.py
  • 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/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.py
  • src/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.sql
  • src/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, and src/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_token method now correctly generates a unique session_id using uuid.uuid4().hex (32-character hex string) and embeds it in the JWT via the standard jti claim. The return type change to tuple[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_id is a 32-character string (matching uuid4().hex output)
  • The jti claim in the decoded JWT matches the returned session_id

94-131: Appropriate use of _ for unused return values.

The tests that don't need the session_id correctly use _token, _, _ or token, _, _ 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 new create_token return signature.

The make_auth_headers helper appropriately unpacks the 3-tuple while only using the token value 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 sessions table addition aligns with the schema definition in src/synthorg/persistence/sqlite/schema.sql and 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 sessions
  • idx_sessions_expires_at: Efficient cleanup of expired sessions
docs/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_store gracefully (startup window, initialization failure)
  • Logs with structured kwargs before denying access
  • Uses hasattr + is not None guards per the lifecycle initialization pattern shown in src/synthorg/api/lifecycle.py

One consideration: tokens without a jti claim (e.g., older tokens issued before this change) will bypass the revocation check entirely due to the if jti guard 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 = True safely enables dynamic user:{id} channels. Server-side access control is already implemented in src/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 sessions
  • VERSION_CONFLICT (4002) properly categorized under Conflict for ETag/If-Match mismatches

Both 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_USER fixture is well-defined with appropriate role and auth method for testing WebSocket message handling. Using HumanRole.CEO ensures the test user has sufficient permissions for all channel subscriptions.


27-206: Test coverage is comprehensive.

All _handle_message invocations correctly pass the _TEST_USER argument, 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 Session and SessionStore exports follow the established re-export pattern, enabling clean imports like from 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_CLEANUP event constant is correctly imported from the domain-specific observability module, following coding guidelines.


206-231: LGTM!

The session cleanup integration is well-implemented:

  • The None check for _session_store properly handles the case before set_session_store() is called during startup (per src/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_certfile is present (pairwise validation in ServerConfig ensures ssl_keyfile is also set)
  • Proxy kwargs are only populated when trusted_proxies is configured
  • The API_TLS_CONFIGURED event provides useful observability

71-83: All uvicorn parameters are valid. The ssl_certfile, ssl_keyfile, ssl_ca_certs, forwarded_allow_ips, and proxy_headers parameters 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 ErrorCode enum 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 where None or empty string ETags skip validation (consistent with "If-Match absent = bypass" per PR objectives)

Good use of pytest.mark.unit marker 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.0 exposure is a good safety signal.

src/synthorg/api/errors.py (1)

202-215: VersionConflictError mapping is consistent.

409 status, conflict category, and VERSION_CONFLICT code 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 frozenset from online_users() prevents external mutation—good defensive design.

src/synthorg/api/auth/session.py (1)

9-40: Well-designed frozen domain model.

The Session model correctly uses frozen=True, allow_inf_nan=False, NotBlankStr for identifiers, and AwareDatetime for timestamps. The choice to allow empty ip_address/user_agent aligns 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_keyfile as sensitive=True. The empty defaults allow TLS to remain optional, and restart_required=True correctly 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 CASCADE for user removal, appropriate defaults, and indexes on user_id and expires_at to support listing and cleanup queries. The revoked INTEGER pattern (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_role which requires _READ_ROLES)

432-434: Presence lifecycle is correctly wired.

connect() is called after successful setup, and disconnect() is in the finally block 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, and extract_user_id() extracts with a safe None return 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. The bus_bridge can publish to user:{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 db fixture correctly applies the schema, seeds prerequisite users for FK constraints, and uses in-memory SQLite for isolation. The deterministic _NOW timestamp via patching ensures reproducible tests.


115-333: Comprehensive test coverage.

Tests cover all SessionStore operations including edge cases (nonexistent sessions, already-revoked, expired sessions). The load_revoked_restores_state test 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, uses NotBlankStr for 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_token return 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.value and entry.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_factory expectation 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 before set_session_store(), eliminating the race window where is_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_user and list_all correctly 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 = 0 clause 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) that datetime.now(UTC).isoformat() produces. All timestamp values stored in the database come from Session fields (which are AwareDatetime), ensuring they include timezone information. Pydantic's AwareDatetime validation succeeds with the parsed timezone-aware datetime objects.

# 1xxx -- auth
UNAUTHORIZED = 1000
FORBIDDEN = 1001
SESSION_REVOKED = 1002
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +224 to +229
except Exception:
logger.warning(
API_APP_STARTUP,
error="Session store init failed (non-fatal)",
exc_info=True,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

@Aureliolo Aureliolo force-pushed the feat/network-hosting-multi-user branch from c3551b2 to ab0e341 Compare April 3, 2026 11:40
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 11:41 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🔵 Trivial

Missing test coverage for session revocation path.

has_session_store = False ensures 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 when has_session_store = True and the session is revoked. Consider adding a test that:

  1. Sets has_session_store = True with a mock session_store
  2. Configures is_revoked(jti) to return True
  3. 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 | 🟡 Minor

Missing 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 VersionConflictError without 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

📥 Commits

Reviewing files that changed from the base of the PR and between c3551b2 and ab0e341.

📒 Files selected for processing (44)
  • CLAUDE.md
  • docs/design/operations.md
  • docs/design/page-structure.md
  • docs/errors.md
  • src/synthorg/api/app.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/auth/controller.py
  • src/synthorg/api/auth/middleware.py
  • src/synthorg/api/auth/presence.py
  • src/synthorg/api/auth/service.py
  • src/synthorg/api/auth/session.py
  • src/synthorg/api/auth/session_store.py
  • src/synthorg/api/channels.py
  • src/synthorg/api/concurrency.py
  • src/synthorg/api/config.py
  • src/synthorg/api/controllers/settings.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/server.py
  • src/synthorg/api/state.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/repositories.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/settings_repo.py
  • src/synthorg/settings/definitions/api.py
  • src/synthorg/settings/service.py
  • tests/unit/api/auth/test_controller.py
  • tests/unit/api/auth/test_middleware.py
  • tests/unit/api/auth/test_presence.py
  • tests/unit/api/auth/test_service.py
  • tests/unit/api/auth/test_session_store.py
  • tests/unit/api/conftest.py
  • tests/unit/api/controllers/test_ws.py
  • tests/unit/api/fakes.py
  • tests/unit/api/test_channels.py
  • tests/unit/api/test_concurrency.py
  • tests/unit/api/test_config.py
  • tests/unit/api/test_errors.py
  • tests/unit/api/test_server.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/persistence/test_protocol.py

Comment on lines +27 to +32
def connect(self, user_id: str) -> None:
"""Record a new WebSocket connection for a user.

Args:
user_id: The connecting user's ID.
"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +34 to +60
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,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +24 to +25
user_agent: Client User-Agent header at login time
(capped at 512 characters).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +50 to +59
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}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +456 to +472
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",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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)
@Aureliolo Aureliolo force-pushed the feat/network-hosting-multi-user branch from ab0e341 to 83b6f65 Compare April 3, 2026 12:16
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 12:17 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (5)
src/synthorg/api/auth/presence.py (2)

34-38: ⚠️ Potential issue | 🟡 Minor

Only 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 at DEBUG. That makes presence telemetry inaccurate and misses the project logging contract. Use the domain event constants, log actual connect/disconnect transitions at INFO, 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 | 🟠 Major

Use NotBlankStr throughout the presence API.

UserPresence still exposes user IDs as plain str in 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 | 🟠 Major

Use 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 | 🟠 Major

Enforce the documented user_agent length bound in the model.

The docstring promises a 512-character cap, but user_agent is 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 constructing Session.

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 | 🟠 Major

Support “expect no row” in the CAS path.

When expected_updated_at is the empty-string sentinel used for resolved settings without a stored override, this branch can only UPDATE, so the first edit of an env/YAML/default-backed setting returns False and 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

📥 Commits

Reviewing files that changed from the base of the PR and between ab0e341 and 83b6f65.

📒 Files selected for processing (44)
  • CLAUDE.md
  • docs/design/operations.md
  • docs/design/page-structure.md
  • docs/errors.md
  • src/synthorg/api/app.py
  • src/synthorg/api/auth/__init__.py
  • src/synthorg/api/auth/controller.py
  • src/synthorg/api/auth/middleware.py
  • src/synthorg/api/auth/presence.py
  • src/synthorg/api/auth/service.py
  • src/synthorg/api/auth/session.py
  • src/synthorg/api/auth/session_store.py
  • src/synthorg/api/channels.py
  • src/synthorg/api/concurrency.py
  • src/synthorg/api/config.py
  • src/synthorg/api/controllers/settings.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/api/errors.py
  • src/synthorg/api/lifecycle.py
  • src/synthorg/api/server.py
  • src/synthorg/api/state.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/repositories.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/settings_repo.py
  • src/synthorg/settings/definitions/api.py
  • src/synthorg/settings/service.py
  • tests/unit/api/auth/test_controller.py
  • tests/unit/api/auth/test_middleware.py
  • tests/unit/api/auth/test_presence.py
  • tests/unit/api/auth/test_service.py
  • tests/unit/api/auth/test_session_store.py
  • tests/unit/api/conftest.py
  • tests/unit/api/controllers/test_ws.py
  • tests/unit/api/fakes.py
  • tests/unit/api/test_channels.py
  • tests/unit/api/test_concurrency.py
  • tests/unit/api/test_config.py
  • tests/unit/api/test_errors.py
  • tests/unit/api/test_server.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/persistence/test_protocol.py

Comment on lines +734 to +746
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,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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:

  1. Returning 403 for non-CEO requesting scope=all, or
  2. 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).

Comment on lines +54 to +56
def __init__(self, db: aiosqlite.Connection) -> None:
self._db = db
self._revoked: set[str] = set()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +98 to +102
logger.debug(
API_SESSION_CREATED,
session_id=session.session_id,
user_id=session.user_id,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +182 to +189
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +65 to +66
@pytest.fixture
async def db() -> AsyncGenerator[aiosqlite.Connection]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
@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.

Comment on lines +55 to +73
@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:") == ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🔵 Trivial

Document the first-write CAS sentinel.

_check_setting_etag() now passes current.updated_at or "" for the “no DB override yet” case, but this public contract only describes matching an existing updated_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 | 🔵 Trivial

Use API_WS_TRANSPORT_ERROR for unsubscribe failures.

The event constant API_WS_DISCONNECTED is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 83b6f65 and 9494734.

📒 Files selected for processing (5)
  • src/synthorg/api/auth/session_store.py
  • src/synthorg/api/controllers/ws.py
  • src/synthorg/observability/events/settings.py
  • src/synthorg/persistence/sqlite/settings_repo.py
  • src/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: No from __future__ import annotations -- Python 3.14 has PEP 649
Use except 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_field for 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 as logger = 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 use logger as the variable name (not _logger or log)
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.py
  • src/synthorg/settings/service.py
  • src/synthorg/persistence/sqlite/settings_repo.py
  • src/synthorg/api/auth/session_store.py
  • src/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.py
  • src/synthorg/settings/service.py
  • src/synthorg/persistence/sqlite/settings_repo.py
  • src/synthorg/api/auth/session_store.py
  • src/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.py
  • 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/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.py
  • 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: 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.channels follows 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_CHANNELS is a frozenset and extract_user_id() properly parses the user: 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_SET and 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_user is 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: Use NotBlankStr for user_id parameter.

The user_id parameter accepts plain str but should use NotBlankStr for 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 use NotBlankStr for session_id parameter.

The revocation logic is correct—idempotent UPDATE with proper in-memory set synchronization and INFO-level logging for the state transition.

The session_id parameter should use NotBlankStr per 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 DEBUG to INFO.

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 NotBlankStr for 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 no revoked = 0 filter, 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_id parameter should use NotBlankStr per 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_id parameter should use NotBlankStr per coding guidelines (see earlier comment).

src/synthorg/observability/events/settings.py (1)

17-17: Nice observability split.

A dedicated SETTINGS_VERSION_CONFLICT event 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.VersionConflictError here reverses the dependency direction (settings → api) and couples this module to HTTP exception definitions. The concern about error handling is valid: unless VersionConflictError is 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.

Comment on lines +416 to +420
# 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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_ch

Then 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
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 13:04 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9494734 and 8ecd1b6.

📒 Files selected for processing (2)
  • src/synthorg/api/controllers/ws.py
  • src/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: No from __future__ import annotations -- Python 3.14 has PEP 649
Use except 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_field for 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 as logger = 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 use logger as the variable name (not _logger or log)
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.py
  • src/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.py
  • src/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_CONFLICT event 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_at as 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 new VersionConflictError exception.


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_CONFLICT event 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_connection and again at line 476 in ws_handler. Consider returning it from _setup_connection to 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_allowed check 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 outer finally block, 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_id to the error log context improves debuggability when transport errors occur.


651-665: LGTM!

The subscription authorization logic correctly:

  1. Always allows the user's own private channel
  2. Validates shared channels against _channel_allowed() before adding to subscriptions
  3. 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.

Comment on lines +544 to +546
from synthorg.api.errors import ( # noqa: PLC0415
VersionConflictError,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

@Aureliolo Aureliolo merged commit 398c378 into main Apr 3, 2026
32 of 34 checks passed
@Aureliolo Aureliolo deleted the feat/network-hosting-multi-user branch April 3, 2026 13:33
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 13:33 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 3, 2026
🤖 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement network hosting and multi-user access

2 participants