remdb 0.3.242__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 (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,422 @@
1
+ """
2
+ Session sharing endpoints.
3
+
4
+ Enables session sharing between users for collaborative access to conversation history.
5
+
6
+ Endpoints:
7
+ POST /api/v1/sessions/{session_id}/share - Share a session with another user
8
+ DELETE /api/v1/sessions/{session_id}/share/{user_id} - Revoke a share (soft delete)
9
+ GET /api/v1/sessions/shared-with-me - Get users sharing sessions with you
10
+ GET /api/v1/sessions/shared-with-me/{user_id}/messages - Get messages from a user's shared sessions
11
+
12
+ See src/rem/models/entities/shared_session.py for full documentation.
13
+ """
14
+
15
+ from typing import Literal
16
+
17
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
18
+ from loguru import logger
19
+ from pydantic import BaseModel, Field
20
+
21
+ from .common import ErrorResponse
22
+
23
+ from ..deps import get_current_user, require_auth
24
+ from ...models.entities import (
25
+ Message,
26
+ SharedSession,
27
+ SharedSessionCreate,
28
+ SharedWithMeResponse,
29
+ SharedWithMeSummary,
30
+ )
31
+ from ...services.postgres import get_postgres_service
32
+ from ...settings import settings
33
+ from ...utils.date_utils import utc_now
34
+
35
+ router = APIRouter(prefix="/api/v1")
36
+
37
+
38
+ async def get_connected_postgres():
39
+ """Get a connected PostgresService instance."""
40
+ pg = get_postgres_service()
41
+ if pg and not pg.pool:
42
+ await pg.connect()
43
+ return pg
44
+
45
+
46
+ # =============================================================================
47
+ # Request/Response Models
48
+ # =============================================================================
49
+
50
+
51
+ class PaginationMetadata(BaseModel):
52
+ """Pagination metadata for paginated responses."""
53
+
54
+ total: int = Field(description="Total number of records matching filters")
55
+ page: int = Field(description="Current page number (1-indexed)")
56
+ page_size: int = Field(description="Number of records per page")
57
+ total_pages: int = Field(description="Total number of pages")
58
+ has_next: bool = Field(description="Whether there are more pages after this one")
59
+ has_previous: bool = Field(description="Whether there are pages before this one")
60
+
61
+
62
+ class SharedMessagesResponse(BaseModel):
63
+ """Response for shared messages query."""
64
+
65
+ object: Literal["list"] = "list"
66
+ data: list[Message] = Field(description="List of messages from shared sessions")
67
+ metadata: PaginationMetadata = Field(description="Pagination metadata")
68
+
69
+
70
+ class ShareSessionResponse(BaseModel):
71
+ """Response after sharing a session."""
72
+
73
+ success: bool = True
74
+ message: str
75
+ share: SharedSession
76
+
77
+
78
+ # =============================================================================
79
+ # Share Session Endpoints
80
+ # =============================================================================
81
+
82
+
83
+ @router.post(
84
+ "/sessions/{session_id}/share",
85
+ response_model=ShareSessionResponse,
86
+ status_code=201,
87
+ tags=["sessions"],
88
+ responses={
89
+ 400: {"model": ErrorResponse, "description": "Session already shared with this user"},
90
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
91
+ },
92
+ )
93
+ async def share_session(
94
+ request: Request,
95
+ session_id: str,
96
+ body: SharedSessionCreate,
97
+ user: dict = Depends(require_auth),
98
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
99
+ ) -> ShareSessionResponse:
100
+ """
101
+ Share a session with another user.
102
+
103
+ Creates a SharedSession record that grants the recipient access to view
104
+ messages in this session.
105
+
106
+ Args:
107
+ session_id: The session to share
108
+ body: Contains shared_with_user_id - the recipient
109
+
110
+ Returns:
111
+ The created SharedSession record
112
+
113
+ Raises:
114
+ 400: Session already shared with this user
115
+ 503: Database not enabled
116
+ """
117
+ if not settings.postgres.enabled:
118
+ raise HTTPException(status_code=503, detail="Database not enabled")
119
+
120
+ current_user_id = user.get("id", "default")
121
+ pg = await get_connected_postgres()
122
+
123
+ # Check if share already exists (active)
124
+ existing = await pg.fetchrow(
125
+ """
126
+ SELECT id FROM shared_sessions
127
+ WHERE tenant_id = $1
128
+ AND session_id = $2
129
+ AND owner_user_id = $3
130
+ AND shared_with_user_id = $4
131
+ AND deleted_at IS NULL
132
+ """,
133
+ x_tenant_id,
134
+ session_id,
135
+ current_user_id,
136
+ body.shared_with_user_id,
137
+ )
138
+
139
+ if existing:
140
+ raise HTTPException(
141
+ status_code=400,
142
+ detail=f"Session '{session_id}' is already shared with user '{body.shared_with_user_id}'",
143
+ )
144
+
145
+ # Create the share
146
+ result = await pg.fetchrow(
147
+ """
148
+ INSERT INTO shared_sessions (session_id, owner_user_id, shared_with_user_id, tenant_id)
149
+ VALUES ($1, $2, $3, $4)
150
+ RETURNING id, session_id, owner_user_id, shared_with_user_id, tenant_id, created_at, updated_at, deleted_at
151
+ """,
152
+ session_id,
153
+ current_user_id,
154
+ body.shared_with_user_id,
155
+ x_tenant_id,
156
+ )
157
+
158
+ share = SharedSession(
159
+ id=result["id"],
160
+ session_id=result["session_id"],
161
+ owner_user_id=result["owner_user_id"],
162
+ shared_with_user_id=result["shared_with_user_id"],
163
+ tenant_id=result["tenant_id"],
164
+ created_at=result["created_at"],
165
+ updated_at=result["updated_at"],
166
+ deleted_at=result["deleted_at"],
167
+ )
168
+
169
+ logger.debug(
170
+ f"User {current_user_id} shared session '{session_id}' with {body.shared_with_user_id}"
171
+ )
172
+
173
+ return ShareSessionResponse(
174
+ success=True,
175
+ message=f"Session shared with {body.shared_with_user_id}",
176
+ share=share,
177
+ )
178
+
179
+
180
+ @router.delete(
181
+ "/sessions/{session_id}/share/{shared_with_user_id}",
182
+ status_code=200,
183
+ tags=["sessions"],
184
+ responses={
185
+ 404: {"model": ErrorResponse, "description": "Share not found"},
186
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
187
+ },
188
+ )
189
+ async def remove_session_share(
190
+ request: Request,
191
+ session_id: str,
192
+ shared_with_user_id: str,
193
+ user: dict = Depends(require_auth),
194
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
195
+ ) -> dict:
196
+ """
197
+ Remove a session share (soft delete).
198
+
199
+ Sets deleted_at on the SharedSession record. The share can be re-created
200
+ later if needed.
201
+
202
+ Args:
203
+ session_id: The session to unshare
204
+ shared_with_user_id: The user to remove access from
205
+
206
+ Returns:
207
+ Success message
208
+
209
+ Raises:
210
+ 404: Share not found
211
+ 503: Database not enabled
212
+ """
213
+ if not settings.postgres.enabled:
214
+ raise HTTPException(status_code=503, detail="Database not enabled")
215
+
216
+ current_user_id = user.get("id", "default")
217
+ pg = await get_connected_postgres()
218
+
219
+ # Soft delete the share
220
+ result = await pg.fetchrow(
221
+ """
222
+ UPDATE shared_sessions
223
+ SET deleted_at = $1, updated_at = $1
224
+ WHERE tenant_id = $2
225
+ AND session_id = $3
226
+ AND owner_user_id = $4
227
+ AND shared_with_user_id = $5
228
+ AND deleted_at IS NULL
229
+ RETURNING id
230
+ """,
231
+ utc_now(),
232
+ x_tenant_id,
233
+ session_id,
234
+ current_user_id,
235
+ shared_with_user_id,
236
+ )
237
+
238
+ if not result:
239
+ raise HTTPException(
240
+ status_code=404,
241
+ detail=f"Share not found for session '{session_id}' with user '{shared_with_user_id}'",
242
+ )
243
+
244
+ logger.debug(
245
+ f"User {current_user_id} removed share for session '{session_id}' with {shared_with_user_id}"
246
+ )
247
+
248
+ return {
249
+ "success": True,
250
+ "message": f"Share removed for user {shared_with_user_id}",
251
+ }
252
+
253
+
254
+ # =============================================================================
255
+ # Shared With Me Endpoints
256
+ # =============================================================================
257
+
258
+
259
+ @router.get(
260
+ "/sessions/shared-with-me",
261
+ response_model=SharedWithMeResponse,
262
+ tags=["sessions"],
263
+ responses={
264
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
265
+ },
266
+ )
267
+ async def get_shared_with_me(
268
+ request: Request,
269
+ page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
270
+ page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
271
+ user: dict = Depends(require_auth),
272
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
273
+ ) -> SharedWithMeResponse:
274
+ """
275
+ Get aggregate summary of users sharing sessions with you.
276
+
277
+ Returns a paginated list of users who have shared sessions with the
278
+ current user, including message counts and date ranges.
279
+
280
+ Each entry shows:
281
+ - user_id, name, email of the person sharing
282
+ - message_count: total messages across all their shared sessions
283
+ - session_count: number of sessions they've shared
284
+ - first_message_at, last_message_at: date range
285
+
286
+ Results are ordered by most recent message first.
287
+ """
288
+ if not settings.postgres.enabled:
289
+ raise HTTPException(status_code=503, detail="Database not enabled")
290
+
291
+ current_user_id = user.get("id", "default")
292
+ pg = await get_connected_postgres()
293
+ offset = (page - 1) * page_size
294
+
295
+ # Get total count
296
+ count_result = await pg.fetchrow(
297
+ "SELECT fn_count_shared_with_me($1, $2) as total",
298
+ x_tenant_id,
299
+ current_user_id,
300
+ )
301
+ total = count_result["total"] if count_result else 0
302
+
303
+ # Get paginated results
304
+ rows = await pg.fetch(
305
+ "SELECT * FROM fn_get_shared_with_me($1, $2, $3, $4)",
306
+ x_tenant_id,
307
+ current_user_id,
308
+ page_size,
309
+ offset,
310
+ )
311
+
312
+ data = [
313
+ SharedWithMeSummary(
314
+ user_id=row["user_id"],
315
+ name=row["name"],
316
+ email=row["email"],
317
+ message_count=row["message_count"],
318
+ session_count=row["session_count"],
319
+ first_message_at=row["first_message_at"],
320
+ last_message_at=row["last_message_at"],
321
+ )
322
+ for row in rows
323
+ ]
324
+
325
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 1
326
+
327
+ return SharedWithMeResponse(
328
+ data=data,
329
+ metadata={
330
+ "total": total,
331
+ "page": page,
332
+ "page_size": page_size,
333
+ "total_pages": total_pages,
334
+ "has_next": page < total_pages,
335
+ "has_previous": page > 1,
336
+ },
337
+ )
338
+
339
+
340
+ @router.get(
341
+ "/sessions/shared-with-me/{owner_user_id}/messages",
342
+ response_model=SharedMessagesResponse,
343
+ tags=["sessions"],
344
+ responses={
345
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
346
+ },
347
+ )
348
+ async def get_shared_messages(
349
+ request: Request,
350
+ owner_user_id: str,
351
+ page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
352
+ page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
353
+ user: dict = Depends(require_auth),
354
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
355
+ ) -> SharedMessagesResponse:
356
+ """
357
+ Get messages from sessions shared by a specific user.
358
+
359
+ Returns paginated messages from all sessions that owner_user_id has
360
+ shared with the current user. Messages are ordered by created_at DESC.
361
+
362
+ Args:
363
+ owner_user_id: The user who shared the sessions
364
+
365
+ Returns:
366
+ Paginated list of Message objects
367
+ """
368
+ if not settings.postgres.enabled:
369
+ raise HTTPException(status_code=503, detail="Database not enabled")
370
+
371
+ current_user_id = user.get("id", "default")
372
+ pg = await get_connected_postgres()
373
+ offset = (page - 1) * page_size
374
+
375
+ # Get total count
376
+ count_result = await pg.fetchrow(
377
+ "SELECT fn_count_shared_messages($1, $2, $3) as total",
378
+ x_tenant_id,
379
+ current_user_id,
380
+ owner_user_id,
381
+ )
382
+ total = count_result["total"] if count_result else 0
383
+
384
+ # Get paginated messages
385
+ rows = await pg.fetch(
386
+ "SELECT * FROM fn_get_shared_messages($1, $2, $3, $4, $5)",
387
+ x_tenant_id,
388
+ current_user_id,
389
+ owner_user_id,
390
+ page_size,
391
+ offset,
392
+ )
393
+
394
+ # Convert to Message objects
395
+ data = [
396
+ Message(
397
+ id=row["id"],
398
+ content=row["content"],
399
+ message_type=row["message_type"],
400
+ session_id=row["session_id"],
401
+ model=row["model"],
402
+ token_count=row["token_count"],
403
+ created_at=row["created_at"],
404
+ metadata=row["metadata"] or {},
405
+ tenant_id=x_tenant_id,
406
+ )
407
+ for row in rows
408
+ ]
409
+
410
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 1
411
+
412
+ return SharedMessagesResponse(
413
+ data=data,
414
+ metadata=PaginationMetadata(
415
+ total=total,
416
+ page=page,
417
+ page_size=page_size,
418
+ total_pages=total_pages,
419
+ has_next=page < total_pages,
420
+ has_previous=page > 1,
421
+ ),
422
+ )
rem/auth/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # OAuth 2.1 Authentication
2
+
3
+ OAuth 2.1 compliant authentication with Google and Microsoft Entra ID.
4
+
5
+ ## Features
6
+
7
+ - **OAuth 2.1 Security Best Practices**
8
+ - PKCE (Proof Key for Code Exchange) - mandatory for all flows
9
+ - State parameter for CSRF protection
10
+ - Nonce for ID token replay protection
11
+ - Token validation with JWKS
12
+
13
+ - **Supported Providers**
14
+ - Google OAuth 2.0 / OIDC
15
+ - Microsoft Entra ID (Azure AD) OIDC
16
+
17
+ - **Minimal Code**
18
+ - Leverages Authlib for standards compliance
19
+ - Authlib handles PKCE, token exchange, JWKS validation
20
+ - Clean integration with FastAPI
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install authlib httpx
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ ### Google OAuth Setup
31
+
32
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
33
+ 2. Create OAuth 2.0 credentials
34
+ 3. Add authorized redirect URI: `http://localhost:8000/api/auth/google/callback`
35
+ 4. Set environment variables:
36
+
37
+ ```bash
38
+ AUTH__ENABLED=true
39
+ AUTH__SESSION_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
40
+
41
+ AUTH__GOOGLE__CLIENT_ID=your-client-id.apps.googleusercontent.com
42
+ AUTH__GOOGLE__CLIENT_SECRET=your-client-secret
43
+ AUTH__GOOGLE__REDIRECT_URI=http://localhost:8000/api/auth/google/callback
44
+ ```
45
+
46
+ ### Microsoft Entra ID Setup
47
+
48
+ 1. Go to [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)
49
+ 2. Register new application
50
+ 3. Create client secret under "Certificates & secrets"
51
+ 4. Add redirect URI: `http://localhost:8000/api/auth/microsoft/callback`
52
+ 5. Add API permissions: Microsoft Graph > User.Read (delegated)
53
+ 6. Set environment variables:
54
+
55
+ ```bash
56
+ AUTH__ENABLED=true
57
+ AUTH__SESSION_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
58
+
59
+ AUTH__MICROSOFT__CLIENT_ID=your-application-id
60
+ AUTH__MICROSOFT__CLIENT_SECRET=your-client-secret
61
+ AUTH__MICROSOFT__REDIRECT_URI=http://localhost:8000/api/auth/microsoft/callback
62
+ AUTH__MICROSOFT__TENANT=common # or your tenant ID
63
+ ```
64
+
65
+ **Tenant options:**
66
+ - `common` - Multi-tenant + personal Microsoft accounts
67
+ - `organizations` - Work/school accounts only
68
+ - `consumers` - Personal Microsoft accounts only
69
+ - `{tenant-id}` - Single tenant (specific organization)
70
+
71
+ ## Usage
72
+
73
+ ### 1. Start the API server
74
+
75
+ ```bash
76
+ cd rem
77
+ uv run python -m rem.api.main
78
+ ```
79
+
80
+ ### 2. Initiate login
81
+
82
+ Navigate to:
83
+ - Google: `http://localhost:8000/api/auth/google/login`
84
+ - Microsoft: `http://localhost:8000/api/auth/microsoft/login`
85
+
86
+ ### 3. OAuth Flow
87
+
88
+ ```
89
+ User Browser API Server OAuth Provider
90
+ | | | |
91
+ |-- Click Login ---->| | |
92
+ | |-- GET /auth/google/login --> |
93
+ | | |-- Generate PKCE ------->|
94
+ | | | (code_verifier) |
95
+ | |<-- Redirect to Google --| |
96
+ |<-- Show Google login --| | |
97
+ | | | |
98
+ |-- Enter credentials --> | |
99
+ | |-- Authorize ----------------------->| |
100
+ | |<-- Redirect with code ----------------| |
101
+ | | | |
102
+ | |-- GET /auth/google/callback?code=xyz ---------->|
103
+ | | |-- Exchange code ------->|
104
+ | | | + code_verifier |
105
+ | | |<-- Tokens --------------|
106
+ | | |-- Validate ID token --->|
107
+ | | | (JWKS) |
108
+ | |<-- Set session cookie --| |
109
+ |<-- Redirect to app ---| | |
110
+ ```
111
+
112
+ ### 4. Access protected endpoints
113
+
114
+ After login, session cookie is set automatically:
115
+
116
+ ```bash
117
+ # Get current user
118
+ curl http://localhost:8000/api/auth/me \
119
+ -H "Cookie: rem_session=..."
120
+
121
+ # Protected API endpoint
122
+ curl http://localhost:8000/api/v1/chat/completions \
123
+ -H "Cookie: rem_session=..." \
124
+ -H "Content-Type: application/json" \
125
+ -d '{
126
+ "model": "anthropic:claude-sonnet-4-5-20250929",
127
+ "messages": [{"role": "user", "content": "Hello"}]
128
+ }'
129
+ ```
130
+
131
+ ### 5. Logout
132
+
133
+ ```bash
134
+ curl -X POST http://localhost:8000/api/auth/logout \
135
+ -H "Cookie: rem_session=..."
136
+ ```
137
+
138
+ ## API Endpoints
139
+
140
+ | Method | Path | Description |
141
+ |--------|------|-------------|
142
+ | GET | `/api/auth/google/login` | Initiate Google OAuth flow |
143
+ | GET | `/api/auth/google/callback` | Google OAuth callback |
144
+ | GET | `/api/auth/microsoft/login` | Initiate Microsoft OAuth flow |
145
+ | GET | `/api/auth/microsoft/callback` | Microsoft OAuth callback |
146
+ | POST | `/api/auth/logout` | Clear session |
147
+ | GET | `/api/auth/me` | Get current user info |
148
+
149
+ ## Security Features
150
+
151
+ ### OAuth 2.1 Compliance
152
+
153
+ - **PKCE**: All flows use code_challenge (S256 method)
154
+ - **State**: CSRF protection on all authorization requests
155
+ - **Nonce**: ID token replay protection
156
+ - **No implicit flow**: Only authorization code flow supported
157
+ - **JWKS validation**: ID tokens validated with provider's public keys
158
+
159
+ ### Session Security
160
+
161
+ - **HTTPOnly cookies**: Session cookies not accessible to JavaScript
162
+ - **SameSite=Lax**: CSRF protection for cookie-based auth
163
+ - **Secure flag**: HTTPS-only cookies in production
164
+ - **Short expiration**: 1 hour session lifetime (configurable)
165
+
166
+ ### Middleware Protection
167
+
168
+ - Protects `/api/v1/*` endpoints
169
+ - Excludes `/api/auth/*` and public endpoints
170
+ - Returns 401 for API requests (JSON)
171
+ - Redirects to login for browser requests
172
+
173
+ ## Provider-Specific Features
174
+
175
+ ### Google
176
+
177
+ - **Hosted domain restriction**: Limit to Google Workspace domain
178
+ - **Offline access**: Request refresh tokens
179
+ - **Incremental authorization**: Add scopes incrementally
180
+
181
+ ```bash
182
+ AUTH__GOOGLE__HOSTED_DOMAIN=example.com # Google Workspace only
183
+ ```
184
+
185
+ ### Microsoft
186
+
187
+ - **Multi-tenant support**: common/organizations/consumers
188
+ - **Conditional access**: Honors Entra ID policies
189
+ - **Microsoft Graph**: Access user profile via Graph API
190
+
191
+ ```bash
192
+ AUTH__MICROSOFT__TENANT=common # Multi-tenant
193
+ AUTH__MICROSOFT__TENANT=organizations # Work/school only
194
+ AUTH__MICROSOFT__TENANT=consumers # Personal accounts
195
+ AUTH__MICROSOFT__TENANT=contoso.com # Single tenant
196
+ ```
197
+
198
+ ## Architecture
199
+
200
+ ```
201
+ rem/src/rem/auth/
202
+ ├── __init__.py # Module exports
203
+ ├── README.md # This file
204
+ ├── middleware.py # FastAPI auth middleware
205
+ ├── providers/ # OAuth provider implementations
206
+ │ ├── __init__.py
207
+ │ ├── base.py # Base OAuth provider (kept for reference)
208
+ │ ├── google.py # Google provider (kept for reference)
209
+ │ └── microsoft.py # Microsoft provider (kept for reference)
210
+ ```
211
+
212
+ **Note**: Provider classes in `providers/` are kept for reference but not used.
213
+ The implementation uses Authlib's built-in provider support via `server_metadata_url`.
214
+
215
+ ## Testing
216
+
217
+ ```bash
218
+ # Test Google login flow
219
+ open http://localhost:8000/api/auth/google/login
220
+
221
+ # Test Microsoft login flow
222
+ open http://localhost:8000/api/auth/microsoft/login
223
+
224
+ # Check current user
225
+ curl http://localhost:8000/api/auth/me
226
+ ```
227
+
228
+ ## Troubleshooting
229
+
230
+ ### "Authentication is disabled"
231
+
232
+ Set `AUTH__ENABLED=true` in environment or `.env` file.
233
+
234
+ ### "Unknown provider: google"
235
+
236
+ Check that `AUTH__GOOGLE__CLIENT_ID` is set. The router only registers providers with valid credentials.
237
+
238
+ ### Redirect URI mismatch
239
+
240
+ Ensure redirect URI in environment matches exactly what's registered with provider:
241
+ - Google: Check Google Cloud Console > Credentials
242
+ - Microsoft: Check Azure Portal > App registrations > Authentication
243
+
244
+ ### PKCE errors
245
+
246
+ Authlib handles PKCE automatically. If you see PKCE errors:
247
+ 1. Clear browser cookies and sessions
248
+ 2. Ensure session middleware is registered before auth router
249
+ 3. Check that `AUTH__SESSION_SECRET` is set
250
+
251
+ ## References
252
+
253
+ - [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11)
254
+ - [OIDC Core](https://openid.net/specs/openid-connect-core-1_0.html)
255
+ - [PKCE RFC](https://datatracker.ietf.org/doc/html/rfc7636)
256
+ - [Authlib Documentation](https://docs.authlib.org/en/latest/)
257
+ - [Google OAuth](https://developers.google.com/identity/protocols/oauth2)
258
+ - [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/)