remdb 0.3.127__py3-none-any.whl → 0.3.172__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (62) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/context.py +81 -3
  4. rem/agentic/context_builder.py +36 -9
  5. rem/agentic/mcp/tool_wrapper.py +132 -15
  6. rem/agentic/providers/phoenix.py +371 -108
  7. rem/agentic/providers/pydantic_ai.py +163 -45
  8. rem/agentic/schema.py +8 -4
  9. rem/api/deps.py +3 -5
  10. rem/api/main.py +22 -3
  11. rem/api/mcp_router/resources.py +15 -10
  12. rem/api/mcp_router/server.py +2 -0
  13. rem/api/mcp_router/tools.py +94 -2
  14. rem/api/middleware/tracking.py +5 -5
  15. rem/api/routers/auth.py +349 -6
  16. rem/api/routers/chat/completions.py +5 -3
  17. rem/api/routers/chat/streaming.py +95 -22
  18. rem/api/routers/messages.py +24 -15
  19. rem/auth/__init__.py +13 -3
  20. rem/auth/jwt.py +352 -0
  21. rem/auth/middleware.py +115 -10
  22. rem/auth/providers/__init__.py +4 -1
  23. rem/auth/providers/email.py +215 -0
  24. rem/cli/commands/configure.py +3 -4
  25. rem/cli/commands/experiments.py +226 -50
  26. rem/cli/commands/session.py +336 -0
  27. rem/cli/dreaming.py +2 -2
  28. rem/cli/main.py +2 -0
  29. rem/models/core/experiment.py +58 -14
  30. rem/models/entities/__init__.py +4 -0
  31. rem/models/entities/ontology.py +1 -1
  32. rem/models/entities/ontology_config.py +1 -1
  33. rem/models/entities/subscriber.py +175 -0
  34. rem/models/entities/user.py +1 -0
  35. rem/schemas/agents/core/agent-builder.yaml +235 -0
  36. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  37. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  38. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  39. rem/services/__init__.py +3 -1
  40. rem/services/content/service.py +4 -3
  41. rem/services/email/__init__.py +10 -0
  42. rem/services/email/service.py +513 -0
  43. rem/services/email/templates.py +360 -0
  44. rem/services/postgres/README.md +38 -0
  45. rem/services/postgres/diff_service.py +19 -3
  46. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  47. rem/services/postgres/repository.py +5 -4
  48. rem/services/session/compression.py +113 -50
  49. rem/services/session/reload.py +14 -7
  50. rem/services/user_service.py +41 -9
  51. rem/settings.py +292 -5
  52. rem/sql/migrations/001_install.sql +1 -1
  53. rem/sql/migrations/002_install_models.sql +91 -91
  54. rem/sql/migrations/005_schema_update.sql +145 -0
  55. rem/utils/README.md +45 -0
  56. rem/utils/files.py +157 -1
  57. rem/utils/schema_loader.py +45 -7
  58. rem/utils/vision.py +1 -1
  59. {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/METADATA +7 -5
  60. {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/RECORD +62 -52
  61. {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
  62. {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
rem/auth/middleware.py CHANGED
@@ -1,17 +1,28 @@
1
1
  """
2
- OAuth Authentication Middleware for FastAPI.
2
+ Authentication Middleware for FastAPI.
3
3
 
4
- Protects API endpoints by requiring valid session.
5
- Supports anonymous access with rate limiting when allow_anonymous=True.
4
+ Protects API endpoints by requiring valid authentication.
5
+ Supports multiple auth methods: JWT, API Key, Session, Dev Token.
6
+ Anonymous access with rate limiting when allow_anonymous=True.
6
7
  MCP endpoints are always protected unless explicitly disabled.
7
8
 
8
9
  Design Pattern:
9
- - Check session for user on protected paths
10
- - Check Bearer token for dev token (non-production only)
10
+ - API Key (X-API-Key): Access control guardrail, NOT user identity
11
+ - JWT (Authorization: Bearer): Primary method for user identity
12
+ - Dev token: Non-production testing (starts with "dev_")
13
+ - Session: Backward compatibility for browser-based auth
11
14
  - MCP paths always require authentication (protected service)
12
- - If allow_anonymous=True: Allow unauthenticated requests (marked as ANONYMOUS tier)
13
- - If allow_anonymous=False: Return 401 for API calls, redirect browsers to login
14
- - Exclude auth endpoints and public paths
15
+
16
+ Authentication Flow:
17
+ 1. If API key enabled: Validate X-API-Key header (access gate)
18
+ 2. Check JWT token for user identity (primary)
19
+ 3. Check dev token for testing (non-production only)
20
+ 4. Check session for user (backward compatibility)
21
+ 5. If allow_anonymous=True: Allow as anonymous (rate-limited)
22
+ 6. If allow_anonymous=False: Return 401 / redirect to login
23
+
24
+ IMPORTANT: API key validates ACCESS, JWT identifies USER.
25
+ Both can be required: API key for access + JWT for user identity.
15
26
 
16
27
  Access Modes (configured in settings.auth):
17
28
  - enabled=true, allow_anonymous=true: Auth available, anonymous gets rate-limited access
@@ -20,6 +31,11 @@ Access Modes (configured in settings.auth):
20
31
  - mcp_requires_auth=true (default): MCP always requires login regardless of allow_anonymous
21
32
  - mcp_requires_auth=false: MCP follows normal allow_anonymous rules (dev only)
22
33
 
34
+ API Key Authentication (configured in settings.api):
35
+ - api_key_enabled=true: Require X-API-Key header for access
36
+ - api_key: The secret key to validate against
37
+ - API key is an ACCESS GATE, not user identity - JWT still needed for user
38
+
23
39
  Dev Token Support (non-production only):
24
40
  - GET /api/auth/dev/token returns a Bearer token for test-user
25
41
  - Include as: Authorization: Bearer dev_<signature>
@@ -82,6 +98,67 @@ class AuthMiddleware(BaseHTTPMiddleware):
82
98
  self.mcp_requires_auth = mcp_requires_auth
83
99
  self.mcp_path = mcp_path
84
100
 
101
+ def _check_api_key(self, request: Request) -> dict | None:
102
+ """
103
+ Check for valid X-API-Key header.
104
+
105
+ Returns:
106
+ API key user dict if valid, None otherwise
107
+ """
108
+ # Only check if API key auth is enabled
109
+ if not settings.api.api_key_enabled:
110
+ return None
111
+
112
+ # Check for X-API-Key header
113
+ api_key = request.headers.get("x-api-key")
114
+ if not api_key:
115
+ return None
116
+
117
+ # Validate against configured API key
118
+ if settings.api.api_key and api_key == settings.api.api_key:
119
+ logger.debug("X-API-Key authenticated")
120
+ return {
121
+ "id": "api-key-user",
122
+ "email": "api@rem.local",
123
+ "name": "API Key User",
124
+ "provider": "api-key",
125
+ "tenant_id": "default",
126
+ "tier": "pro", # API key users get full access
127
+ "roles": ["user"],
128
+ }
129
+
130
+ # Invalid API key
131
+ logger.warning("Invalid X-API-Key provided")
132
+ return None
133
+
134
+ def _check_jwt_token(self, request: Request) -> dict | None:
135
+ """
136
+ Check for valid JWT in Authorization header.
137
+
138
+ Returns:
139
+ User dict if valid JWT, None otherwise
140
+ """
141
+ auth_header = request.headers.get("authorization", "")
142
+ if not auth_header.startswith("Bearer "):
143
+ return None
144
+
145
+ token = auth_header[7:] # Strip "Bearer "
146
+
147
+ # Skip dev tokens (handled separately)
148
+ if token.startswith("dev_"):
149
+ return None
150
+
151
+ # Verify JWT token
152
+ from .jwt import get_jwt_service
153
+ jwt_service = get_jwt_service()
154
+ user = jwt_service.verify_token(token)
155
+
156
+ if user:
157
+ logger.debug(f"JWT authenticated: {user.get('email')}")
158
+ return user
159
+
160
+ return None
161
+
85
162
  def _check_dev_token(self, request: Request) -> dict | None:
86
163
  """
87
164
  Check for valid dev token in Authorization header (non-production only).
@@ -105,7 +182,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
105
182
  # Verify dev token
106
183
  from ..api.routers.dev import verify_dev_token
107
184
  if verify_dev_token(token):
108
- logger.debug(f"Dev token authenticated as test-user")
185
+ logger.debug("Dev token authenticated as test-user")
109
186
  return {
110
187
  "id": "test-user",
111
188
  "email": "test@rem.local",
@@ -142,6 +219,34 @@ class AuthMiddleware(BaseHTTPMiddleware):
142
219
  if not is_protected or is_excluded:
143
220
  return await call_next(request)
144
221
 
222
+ # API key validation (access control, not user identity)
223
+ # API key is a guardrail for access - JWT identifies the actual user
224
+ if settings.api.api_key_enabled:
225
+ api_key = request.headers.get("x-api-key")
226
+ if not api_key:
227
+ logger.debug(f"Missing X-API-Key for: {path}")
228
+ return JSONResponse(
229
+ status_code=401,
230
+ content={"detail": "API key required. Include X-API-Key header."},
231
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
232
+ )
233
+ if api_key != settings.api.api_key:
234
+ logger.warning(f"Invalid X-API-Key for: {path}")
235
+ return JSONResponse(
236
+ status_code=401,
237
+ content={"detail": "Invalid API key"},
238
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
239
+ )
240
+ logger.debug("X-API-Key validated for access")
241
+ # API key valid - continue to check JWT for user identity
242
+
243
+ # Check for JWT token in Authorization header (primary user identity)
244
+ jwt_user = self._check_jwt_token(request)
245
+ if jwt_user:
246
+ request.state.user = jwt_user
247
+ request.state.is_anonymous = False
248
+ return await call_next(request)
249
+
145
250
  # Check for dev token (non-production only)
146
251
  dev_user = self._check_dev_token(request)
147
252
  if dev_user:
@@ -149,7 +254,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
149
254
  request.state.is_anonymous = False
150
255
  return await call_next(request)
151
256
 
152
- # Check for valid session
257
+ # Check for valid session (backward compatibility)
153
258
  user = request.session.get("user")
154
259
 
155
260
  if user:
@@ -1,6 +1,7 @@
1
- """OAuth provider implementations."""
1
+ """Authentication provider implementations."""
2
2
 
3
3
  from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
4
+ from .email import EmailAuthProvider, EmailAuthResult
4
5
  from .google import GoogleOAuthProvider
5
6
  from .microsoft import MicrosoftOAuthProvider
6
7
 
@@ -8,6 +9,8 @@ __all__ = [
8
9
  "OAuthProvider",
9
10
  "OAuthTokens",
10
11
  "OAuthUserInfo",
12
+ "EmailAuthProvider",
13
+ "EmailAuthResult",
11
14
  "GoogleOAuthProvider",
12
15
  "MicrosoftOAuthProvider",
13
16
  ]
@@ -0,0 +1,215 @@
1
+ """
2
+ Email Authentication Provider.
3
+
4
+ Passwordless authentication using email verification codes.
5
+ Unlike OAuth providers, this handles the full flow internally.
6
+
7
+ Flow:
8
+ 1. User requests login with email address
9
+ 2. System generates code, upserts user, sends email
10
+ 3. User enters code
11
+ 4. System verifies code and creates session
12
+
13
+ Design:
14
+ - Uses EmailService for sending codes
15
+ - Creates users with deterministic UUID from email hash
16
+ - Stores challenge in user metadata
17
+ - No external OAuth dependencies
18
+ """
19
+
20
+ from typing import TYPE_CHECKING
21
+ from pydantic import BaseModel, Field
22
+ from loguru import logger
23
+
24
+ from ...services.email import EmailService
25
+
26
+ if TYPE_CHECKING:
27
+ from ...services.postgres import PostgresService
28
+
29
+
30
+ class EmailAuthResult(BaseModel):
31
+ """Result of email authentication operations."""
32
+
33
+ success: bool = Field(description="Whether operation succeeded")
34
+ email: str = Field(description="Email address")
35
+ user_id: str | None = Field(default=None, description="User ID if authenticated")
36
+ error: str | None = Field(default=None, description="Error message if failed")
37
+ message: str | None = Field(default=None, description="User-friendly message")
38
+
39
+
40
+ class EmailAuthProvider:
41
+ """
42
+ Email-based passwordless authentication provider.
43
+
44
+ Handles the complete email login flow:
45
+ 1. send_code() - Generate and send verification code
46
+ 2. verify_code() - Verify code and return user info
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ email_service: EmailService | None = None,
52
+ template_kwargs: dict | None = None,
53
+ ):
54
+ """
55
+ Initialize EmailAuthProvider.
56
+
57
+ Args:
58
+ email_service: EmailService instance (creates new one if not provided)
59
+ template_kwargs: Customization for email templates (colors, branding, etc.)
60
+ """
61
+ self._email_service = email_service or EmailService()
62
+ self._template_kwargs = template_kwargs or {}
63
+
64
+ @property
65
+ def is_configured(self) -> bool:
66
+ """Check if email auth is properly configured."""
67
+ return self._email_service.is_configured
68
+
69
+ async def send_code(
70
+ self,
71
+ email: str,
72
+ db: "PostgresService",
73
+ tenant_id: str = "default",
74
+ ) -> EmailAuthResult:
75
+ """
76
+ Send a verification code to an email address.
77
+
78
+ Creates user if not exists (using deterministic UUID from email).
79
+ Stores code in user metadata.
80
+
81
+ Args:
82
+ email: Email address to send code to
83
+ db: PostgresService instance
84
+ tenant_id: Tenant identifier
85
+
86
+ Returns:
87
+ EmailAuthResult with success status
88
+ """
89
+ if not self.is_configured:
90
+ return EmailAuthResult(
91
+ success=False,
92
+ email=email,
93
+ error="Email service not configured",
94
+ message="Email login is not available. Please try another method.",
95
+ )
96
+
97
+ try:
98
+ result = await self._email_service.send_login_code(
99
+ email=email,
100
+ db=db,
101
+ tenant_id=tenant_id,
102
+ template_kwargs=self._template_kwargs,
103
+ )
104
+
105
+ if result["success"]:
106
+ return EmailAuthResult(
107
+ success=True,
108
+ email=email,
109
+ user_id=result["user_id"],
110
+ message=f"Verification code sent to {email}. Check your inbox.",
111
+ )
112
+ else:
113
+ return EmailAuthResult(
114
+ success=False,
115
+ email=email,
116
+ error=result.get("error", "Failed to send code"),
117
+ message="Failed to send verification code. Please try again.",
118
+ )
119
+
120
+ except Exception as e:
121
+ logger.error(f"Error sending login code: {e}")
122
+ return EmailAuthResult(
123
+ success=False,
124
+ email=email,
125
+ error=str(e),
126
+ message="An error occurred. Please try again.",
127
+ )
128
+
129
+ async def verify_code(
130
+ self,
131
+ email: str,
132
+ code: str,
133
+ db: "PostgresService",
134
+ tenant_id: str = "default",
135
+ ) -> EmailAuthResult:
136
+ """
137
+ Verify a login code and authenticate user.
138
+
139
+ Args:
140
+ email: Email address
141
+ code: 6-digit verification code
142
+ db: PostgresService instance
143
+ tenant_id: Tenant identifier
144
+
145
+ Returns:
146
+ EmailAuthResult with user_id if successful
147
+ """
148
+ try:
149
+ result = await self._email_service.verify_login_code(
150
+ email=email,
151
+ code=code,
152
+ db=db,
153
+ tenant_id=tenant_id,
154
+ )
155
+
156
+ if result["valid"]:
157
+ return EmailAuthResult(
158
+ success=True,
159
+ email=email,
160
+ user_id=result["user_id"],
161
+ message="Successfully authenticated!",
162
+ )
163
+ else:
164
+ error = result.get("error", "Invalid code")
165
+ # User-friendly error messages
166
+ if error == "Login code expired":
167
+ message = "Your code has expired. Please request a new one."
168
+ elif error == "Invalid login code":
169
+ message = "Invalid code. Please check and try again."
170
+ elif error == "No login code requested":
171
+ message = "No code was requested for this email. Please request a new code."
172
+ elif error == "User not found":
173
+ message = "Email not found. Please request a login code first."
174
+ else:
175
+ message = "Verification failed. Please try again."
176
+
177
+ return EmailAuthResult(
178
+ success=False,
179
+ email=email,
180
+ error=error,
181
+ message=message,
182
+ )
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error verifying login code: {e}")
186
+ return EmailAuthResult(
187
+ success=False,
188
+ email=email,
189
+ error=str(e),
190
+ message="An error occurred. Please try again.",
191
+ )
192
+
193
+ def get_user_dict(self, email: str, user_id: str) -> dict:
194
+ """
195
+ Create a user dict for session storage.
196
+
197
+ Compatible with OAuth user format for consistent session handling.
198
+
199
+ Args:
200
+ email: User's email
201
+ user_id: User's UUID
202
+
203
+ Returns:
204
+ User dict for session
205
+ """
206
+ return {
207
+ "id": user_id,
208
+ "email": email,
209
+ "email_verified": True, # Email is verified through code
210
+ "name": email.split("@")[0], # Use email prefix as name
211
+ "provider": "email",
212
+ "tenant_id": "default",
213
+ "tier": "free", # Email users start at free tier
214
+ "roles": ["user"],
215
+ }
@@ -110,7 +110,7 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
110
110
  config = {}
111
111
 
112
112
  # Default values
113
- default_model = "anthropic:claude-sonnet-4-5-20250929"
113
+ default_model = "openai:gpt-4.1"
114
114
  default_temperature = 0.5
115
115
 
116
116
  if use_defaults:
@@ -124,9 +124,9 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
124
124
  # Default model
125
125
  click.echo("\nDefault LLM model (format: provider:model-id)")
126
126
  click.echo("Examples:")
127
+ click.echo(" - openai:gpt-4.1")
127
128
  click.echo(" - anthropic:claude-sonnet-4-5-20250929")
128
- click.echo(" - openai:gpt-4o")
129
- click.echo(" - openai:gpt-4o-mini")
129
+ click.echo(" - openai:gpt-4.1-mini")
130
130
 
131
131
  config["default_model"] = click.prompt(
132
132
  "Default model", default=default_model
@@ -422,7 +422,6 @@ def configure_command(install: bool, claude_desktop: bool, show: bool, edit: boo
422
422
 
423
423
  try:
424
424
  import shutil
425
- from pathlib import Path
426
425
  from fastmcp.mcp_config import update_config_file, StdioMCPServer
427
426
 
428
427
  # Find Claude Desktop config path