codeframe-ai 0.9.0__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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,277 @@
1
+ """Control-plane database management for CodeFRAME.
2
+
3
+ The global database is a **control-plane store** only: auth (users/accounts/
4
+ sessions/verification), API keys, audit logs, interactive sessions, and token
5
+ usage. All v2 domain data (tasks/blockers/PRD/...) lives in the per-workspace
6
+ ``.codeframe/state.db`` via ``codeframe.core.workspace`` — not here.
7
+
8
+ The class acts as a thin facade, delegating to the surviving control-plane
9
+ repositories. Supports both synchronous (sqlite3) and asynchronous (aiosqlite)
10
+ operations.
11
+ """
12
+
13
+ import contextlib
14
+ import os
15
+ import sqlite3
16
+ import threading
17
+ from pathlib import Path
18
+ from typing import Optional
19
+ import logging
20
+
21
+ import asyncio
22
+ import aiosqlite
23
+
24
+ from codeframe.platform_store.schema_manager import SchemaManager
25
+ from codeframe.platform_store.repositories import (
26
+ TokenRepository,
27
+ AuditRepository,
28
+ APIKeyRepository,
29
+ WorkspaceRegistryRepository,
30
+ )
31
+ from codeframe.platform_store.repositories.interactive_sessions import InteractiveSessionRepository
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Audit verbosity configuration
36
+ AUDIT_VERBOSITY = os.getenv("AUDIT_VERBOSITY", "low").lower()
37
+ if AUDIT_VERBOSITY not in ("low", "high"):
38
+ logger.warning(f"Invalid AUDIT_VERBOSITY='{AUDIT_VERBOSITY}', defaulting to 'low'")
39
+ AUDIT_VERBOSITY = "low"
40
+
41
+
42
+ class Database:
43
+ """SQLite manager for the global control-plane store.
44
+
45
+ Repositories:
46
+ - api_keys: API key issuance and lookup
47
+ - audit_logs: Audit logging
48
+ - interactive_sessions: Interactive agent session records
49
+ - token_usage: LLM token usage tracking (also used per-workspace)
50
+ """
51
+
52
+ def __init__(self, db_path: Path | str):
53
+ """Initialize database manager.
54
+
55
+ Args:
56
+ db_path: Path to SQLite database file or ":memory:"
57
+ """
58
+ self.db_path = Path(db_path) if db_path != ":memory:" else db_path
59
+ self.conn: Optional[sqlite3.Connection] = None
60
+ self._async_conn: Optional[aiosqlite.Connection] = None
61
+ self._async_lock = asyncio.Lock()
62
+ self._sync_lock = threading.RLock() # Reentrant lock for thread-safe access
63
+
64
+ # Control-plane repositories (set after connections are created)
65
+ self.token_usage: Optional[TokenRepository] = None
66
+ self.audit_logs: Optional[AuditRepository] = None
67
+ self.api_keys: Optional[APIKeyRepository] = None
68
+ self.interactive_sessions: Optional[InteractiveSessionRepository] = None
69
+ self.workspace_registry: Optional[WorkspaceRegistryRepository] = None
70
+
71
+ def initialize(self) -> None:
72
+ """Initialize database schema and repositories."""
73
+ # Create parent directories if needed
74
+ if self.db_path != ":memory:":
75
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
76
+
77
+ # Create sync connection
78
+ self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
79
+ self.conn.row_factory = sqlite3.Row
80
+ self.conn.execute("PRAGMA foreign_keys = ON")
81
+ # Enable WAL mode for better concurrent access (allows reads during writes)
82
+ self.conn.execute("PRAGMA journal_mode = WAL")
83
+ # Set busy timeout to handle concurrent access contention
84
+ self.conn.execute("PRAGMA busy_timeout = 5000")
85
+
86
+ # Create schema using SchemaManager
87
+ schema_mgr = SchemaManager(self.conn)
88
+ schema_mgr.create_schema()
89
+
90
+ # Initialize all repositories with sync connection
91
+ self._initialize_repositories()
92
+
93
+ def _initialize_repositories(self) -> None:
94
+ """Initialize all repository instances."""
95
+ # Pass both sync and async connections to support mixed operations.
96
+ # Also pass self (Database instance) for cross-repository operations,
97
+ # and sync_lock for thread-safe access to the shared connection.
98
+ self.token_usage = TokenRepository(sync_conn=self.conn, async_conn=self._async_conn, database=self, sync_lock=self._sync_lock)
99
+ self.audit_logs = AuditRepository(sync_conn=self.conn, async_conn=self._async_conn, database=self, sync_lock=self._sync_lock)
100
+ self.api_keys = APIKeyRepository(sync_conn=self.conn, async_conn=self._async_conn, database=self, sync_lock=self._sync_lock)
101
+ self.interactive_sessions = InteractiveSessionRepository(sync_conn=self.conn, async_conn=self._async_conn, database=self, sync_lock=self._sync_lock)
102
+ self.workspace_registry = WorkspaceRegistryRepository(sync_conn=self.conn, async_conn=self._async_conn, database=self, sync_lock=self._sync_lock)
103
+
104
+ # Connection management methods
105
+ def close(self) -> None:
106
+ """Close database connection (sync only)."""
107
+ if self.conn:
108
+ self.conn.close()
109
+ self.conn = None
110
+
111
+ async def close_async(self) -> None:
112
+ """Close async database connection."""
113
+ if self._async_conn:
114
+ await self._async_conn.close()
115
+ self._async_conn = None
116
+
117
+ async def close_all(self) -> None:
118
+ """Close both sync and async database connections."""
119
+ self.close()
120
+ await self.close_async()
121
+
122
+ def __del__(self) -> None:
123
+ """Destructor with warning for unclosed connections."""
124
+ if self._async_conn is not None:
125
+ logger.warning(
126
+ f"Database async connection for {self.db_path} was not explicitly closed. "
127
+ "Use 'async with db:' or call close_async() to properly close async connections."
128
+ )
129
+ if self.conn is not None:
130
+ self.close()
131
+
132
+ async def initialize_async(self) -> None:
133
+ """Explicitly initialize the async database connection."""
134
+ async with self._async_lock:
135
+ if self._async_conn is None:
136
+ self._async_conn = await aiosqlite.connect(str(self.db_path))
137
+ self._async_conn.row_factory = aiosqlite.Row
138
+ # Match sync connection pragmas for consistency
139
+ await self._async_conn.execute("PRAGMA foreign_keys = ON")
140
+ await self._async_conn.execute("PRAGMA journal_mode = WAL")
141
+ await self._async_conn.execute("PRAGMA busy_timeout = 5000")
142
+ logger.debug(f"Async connection initialized for {self.db_path}")
143
+ # Update repository async connections
144
+ if self.token_usage:
145
+ self._update_repository_async_connections()
146
+
147
+ def _update_repository_async_connections(self) -> None:
148
+ """Update async connections in all repositories."""
149
+ for repo in [self.token_usage, self.audit_logs, self.api_keys, self.interactive_sessions, self.workspace_registry]:
150
+ if repo:
151
+ repo._async_conn = self._async_conn
152
+
153
+ async def _get_async_conn(self) -> aiosqlite.Connection:
154
+ """Get async connection with health check and automatic reconnection."""
155
+ async with self._async_lock:
156
+ if self._async_conn is None:
157
+ self._async_conn = await aiosqlite.connect(str(self.db_path))
158
+ self._async_conn.row_factory = aiosqlite.Row
159
+ # Match sync connection pragmas for consistency
160
+ await self._async_conn.execute("PRAGMA foreign_keys = ON")
161
+ await self._async_conn.execute("PRAGMA journal_mode = WAL")
162
+ await self._async_conn.execute("PRAGMA busy_timeout = 5000")
163
+ logger.debug(f"Async connection created (lazy init) for {self.db_path}")
164
+ self._update_repository_async_connections()
165
+ return self._async_conn
166
+
167
+ try:
168
+ await self._async_conn.execute("SELECT 1")
169
+ return self._async_conn
170
+ except Exception as e:
171
+ logger.warning(f"Async connection health check failed: {e}, reconnecting...")
172
+ try:
173
+ await self._async_conn.close()
174
+ except Exception:
175
+ pass
176
+
177
+ self._async_conn = await aiosqlite.connect(str(self.db_path))
178
+ self._async_conn.row_factory = aiosqlite.Row
179
+ # Match sync connection pragmas for consistency
180
+ await self._async_conn.execute("PRAGMA foreign_keys = ON")
181
+ await self._async_conn.execute("PRAGMA journal_mode = WAL")
182
+ await self._async_conn.execute("PRAGMA busy_timeout = 5000")
183
+ logger.info(f"Async connection reconnected for {self.db_path}")
184
+ self._update_repository_async_connections()
185
+ return self._async_conn
186
+
187
+ # Context managers
188
+ def __enter__(self) -> "Database":
189
+ """Context manager entry."""
190
+ if not self.conn:
191
+ self.initialize()
192
+ return self
193
+
194
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
195
+ """Context manager exit."""
196
+ self.close()
197
+
198
+ async def __aenter__(self) -> "Database":
199
+ """Async context manager entry."""
200
+ if not self.conn:
201
+ self.initialize()
202
+ await self.initialize_async()
203
+ return self
204
+
205
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
206
+ """Async context manager exit."""
207
+ await self.close_async()
208
+
209
+ @contextlib.contextmanager
210
+ def transaction(self):
211
+ """Context manager for explicit transaction control.
212
+
213
+ Yields:
214
+ self: The database instance for chaining operations
215
+
216
+ Raises:
217
+ RuntimeError: If called while already inside a transaction
218
+ """
219
+ if not self.conn:
220
+ self.initialize()
221
+
222
+ # Acquire reentrant lock to ensure thread-safe access to connection state
223
+ with self._sync_lock:
224
+ # Guard against nested transactions
225
+ if self.conn.in_transaction:
226
+ raise RuntimeError(
227
+ "Cannot start a nested transaction. "
228
+ "Complete the current transaction first."
229
+ )
230
+
231
+ # SQLite uses autocommit by default; disable it for this transaction
232
+ old_isolation = self.conn.isolation_level
233
+ self.conn.isolation_level = None # Manual transaction mode
234
+ cursor = self.conn.cursor()
235
+
236
+ try:
237
+ cursor.execute("BEGIN")
238
+ yield self
239
+ self.conn.commit()
240
+ except Exception:
241
+ # Only rollback if a transaction was actually started
242
+ if self.conn.in_transaction:
243
+ self.conn.rollback()
244
+ raise
245
+ finally:
246
+ self.conn.isolation_level = old_isolation
247
+
248
+ # ----- Token usage (dual-use facade) -----
249
+ # Note: ``token_usage`` is the one repository whose backing table is NOT in
250
+ # ``SchemaManager`` (control-plane). It is also created by the per-workspace
251
+ # schema in ``core/workspace.py``, and ``react_agent``/``stats_commands``
252
+ # instantiate ``Database(workspace.db_path)`` to record token usage via
253
+ # ``MetricsTracker``. Keep the delegations alongside the control-plane ones.
254
+ def save_token_usage(self, *args, **kwargs):
255
+ """Delegate to token_usage.save_token_usage()."""
256
+ return self.token_usage.save_token_usage(*args, **kwargs)
257
+
258
+ def get_token_usage(self, *args, **kwargs):
259
+ """Delegate to token_usage.get_token_usage()."""
260
+ return self.token_usage.get_token_usage(*args, **kwargs)
261
+
262
+ def get_task_token_summary(self, *args, **kwargs):
263
+ """Delegate to token_usage.get_task_token_summary()."""
264
+ return self.token_usage.get_task_token_summary(*args, **kwargs)
265
+
266
+ def get_batch_token_usage(self, *args, **kwargs):
267
+ """Delegate to token_usage.get_batch_token_usage()."""
268
+ return self.token_usage.get_batch_token_usage(*args, **kwargs)
269
+
270
+ def get_workspace_token_usage(self, *args, **kwargs):
271
+ """Delegate to token_usage.get_workspace_token_usage()."""
272
+ return self.token_usage.get_workspace_token_usage(*args, **kwargs)
273
+
274
+ # ----- Audit log -----
275
+ def create_audit_log(self, *args, **kwargs):
276
+ """Delegate to audit_logs.create_audit_log()."""
277
+ return self.audit_logs.create_audit_log(*args, **kwargs)
@@ -0,0 +1,24 @@
1
+ """Control-plane repository exports.
2
+
3
+ Only the repositories backing the global control-plane store survive: API keys,
4
+ audit logs, and token usage (plus the shared base). The v1 domain repositories
5
+ (projects/issues/tasks/agents/...) were removed with the v2 cleanup — that data
6
+ now lives per-workspace via ``codeframe.core.workspace``. The interactive-session
7
+ repository is imported directly from its module by ``database.py``.
8
+ """
9
+
10
+ from codeframe.platform_store.repositories.base import BaseRepository
11
+ from codeframe.platform_store.repositories.token_repository import TokenRepository
12
+ from codeframe.platform_store.repositories.audit_repository import AuditRepository
13
+ from codeframe.platform_store.repositories.api_key_repository import APIKeyRepository
14
+ from codeframe.platform_store.repositories.workspace_registry_repository import (
15
+ WorkspaceRegistryRepository,
16
+ )
17
+
18
+ __all__ = [
19
+ "BaseRepository",
20
+ "TokenRepository",
21
+ "AuditRepository",
22
+ "APIKeyRepository",
23
+ "WorkspaceRegistryRepository",
24
+ ]
@@ -0,0 +1,245 @@
1
+ """Repository for API key database operations.
2
+
3
+ Handles CRUD operations for API keys used for programmatic server access.
4
+ """
5
+
6
+ import json
7
+ import uuid
8
+ from datetime import datetime, timezone
9
+ from typing import Optional, Dict, Any, List
10
+ import logging
11
+
12
+ from codeframe.platform_store.repositories.base import BaseRepository
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class APIKeyRepository(BaseRepository):
18
+ """Repository for API key operations."""
19
+
20
+ def create(
21
+ self,
22
+ user_id: int,
23
+ name: str,
24
+ key_hash: str,
25
+ prefix: str,
26
+ scopes: List[str],
27
+ expires_at: Optional[datetime] = None,
28
+ ) -> str:
29
+ """Create a new API key record.
30
+
31
+ Args:
32
+ user_id: Owner user ID
33
+ name: Human-readable name for the key
34
+ key_hash: SHA256 or bcrypt hash of the full key
35
+ prefix: First 12 chars for efficient lookup
36
+ scopes: List of permission scopes
37
+ expires_at: Optional expiration timestamp
38
+
39
+ Returns:
40
+ Generated key ID (UUID)
41
+ """
42
+ key_id = str(uuid.uuid4())
43
+ now = datetime.now(timezone.utc).isoformat()
44
+
45
+ # Normalize expires_at to UTC for consistent string comparison
46
+ expires_at_utc = None
47
+ if expires_at:
48
+ if expires_at.tzinfo is None:
49
+ # Treat naive datetime as UTC
50
+ expires_at = expires_at.replace(tzinfo=timezone.utc)
51
+ expires_at_utc = expires_at.astimezone(timezone.utc).isoformat()
52
+
53
+ self._execute(
54
+ """
55
+ INSERT INTO api_keys (
56
+ id, user_id, name, key_hash, prefix, scopes,
57
+ created_at, expires_at, is_active
58
+ )
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
60
+ """,
61
+ (
62
+ key_id,
63
+ user_id,
64
+ name,
65
+ key_hash,
66
+ prefix,
67
+ json.dumps(scopes),
68
+ now,
69
+ expires_at_utc,
70
+ ),
71
+ )
72
+ self._commit()
73
+
74
+ logger.debug(f"Created API key {key_id} for user {user_id}")
75
+ return key_id
76
+
77
+ def get_by_id(self, key_id: str) -> Optional[Dict[str, Any]]:
78
+ """Get an API key by its ID.
79
+
80
+ Args:
81
+ key_id: The key's UUID
82
+
83
+ Returns:
84
+ Key record dict or None if not found
85
+ """
86
+ row = self._fetchone(
87
+ """
88
+ SELECT id, user_id, name, key_hash, prefix, scopes,
89
+ created_at, last_used_at, expires_at, is_active
90
+ FROM api_keys
91
+ WHERE id = ?
92
+ """,
93
+ (key_id,),
94
+ )
95
+
96
+ if row is None:
97
+ return None
98
+
99
+ return self._row_to_api_key(row)
100
+
101
+ def get_by_prefix(self, prefix: str) -> Optional[Dict[str, Any]]:
102
+ """Get an active API key by its prefix.
103
+
104
+ Only returns active, non-expired keys for authentication.
105
+
106
+ Args:
107
+ prefix: The key prefix (first 12 characters)
108
+
109
+ Returns:
110
+ Key record dict or None if not found/inactive
111
+ """
112
+ now = datetime.now(timezone.utc).isoformat()
113
+
114
+ row = self._fetchone(
115
+ """
116
+ SELECT id, user_id, name, key_hash, prefix, scopes,
117
+ created_at, last_used_at, expires_at, is_active
118
+ FROM api_keys
119
+ WHERE prefix = ?
120
+ AND is_active = 1
121
+ AND (expires_at IS NULL OR expires_at > ?)
122
+ """,
123
+ (prefix, now),
124
+ )
125
+
126
+ if row is None:
127
+ return None
128
+
129
+ return self._row_to_api_key(row)
130
+
131
+ def list_user_keys(self, user_id: int) -> List[Dict[str, Any]]:
132
+ """List all API keys for a user (active and inactive).
133
+
134
+ Does NOT include key_hash for security.
135
+
136
+ Args:
137
+ user_id: The user's ID
138
+
139
+ Returns:
140
+ List of key records (without key_hash)
141
+ """
142
+ rows = self._fetchall(
143
+ """
144
+ SELECT id, user_id, name, prefix, scopes,
145
+ created_at, last_used_at, expires_at, is_active
146
+ FROM api_keys
147
+ WHERE user_id = ?
148
+ ORDER BY created_at DESC
149
+ """,
150
+ (user_id,),
151
+ )
152
+
153
+ return [self._row_to_api_key_safe(row) for row in rows]
154
+
155
+ def update_last_used(self, key_id: str) -> None:
156
+ """Update the last_used_at timestamp.
157
+
158
+ Called after successful authentication.
159
+
160
+ Args:
161
+ key_id: The key's UUID
162
+ """
163
+ now = datetime.now(timezone.utc).isoformat()
164
+
165
+ self._execute(
166
+ """
167
+ UPDATE api_keys
168
+ SET last_used_at = ?
169
+ WHERE id = ?
170
+ """,
171
+ (now, key_id),
172
+ )
173
+ self._commit()
174
+
175
+ def revoke(self, key_id: str, user_id: int) -> bool:
176
+ """Revoke an API key (soft delete).
177
+
178
+ Args:
179
+ key_id: The key's UUID
180
+ user_id: The user attempting to revoke (must be owner)
181
+
182
+ Returns:
183
+ True if revoked, False if not found or not owned
184
+ """
185
+ cursor = self._execute(
186
+ """
187
+ UPDATE api_keys
188
+ SET is_active = 0
189
+ WHERE id = ? AND user_id = ?
190
+ """,
191
+ (key_id, user_id),
192
+ )
193
+ self._commit()
194
+
195
+ return cursor.rowcount > 0
196
+
197
+ def delete(self, key_id: str, user_id: int) -> bool:
198
+ """Hard delete an API key.
199
+
200
+ Args:
201
+ key_id: The key's UUID
202
+ user_id: The user attempting to delete (must be owner)
203
+
204
+ Returns:
205
+ True if deleted, False if not found or not owned
206
+ """
207
+ cursor = self._execute(
208
+ """
209
+ DELETE FROM api_keys
210
+ WHERE id = ? AND user_id = ?
211
+ """,
212
+ (key_id, user_id),
213
+ )
214
+ self._commit()
215
+
216
+ return cursor.rowcount > 0
217
+
218
+ def _row_to_api_key(self, row) -> Dict[str, Any]:
219
+ """Convert database row to API key dict (includes hash)."""
220
+ return {
221
+ "id": row["id"],
222
+ "user_id": row["user_id"],
223
+ "name": row["name"],
224
+ "key_hash": row["key_hash"],
225
+ "prefix": row["prefix"],
226
+ "scopes": json.loads(row["scopes"]),
227
+ "created_at": self._ensure_rfc3339(row["created_at"]),
228
+ "last_used_at": self._ensure_rfc3339(row["last_used_at"]) if row["last_used_at"] else None,
229
+ "expires_at": self._ensure_rfc3339(row["expires_at"]) if row["expires_at"] else None,
230
+ "is_active": bool(row["is_active"]),
231
+ }
232
+
233
+ def _row_to_api_key_safe(self, row) -> Dict[str, Any]:
234
+ """Convert database row to API key dict (excludes hash for listing)."""
235
+ return {
236
+ "id": row["id"],
237
+ "user_id": row["user_id"],
238
+ "name": row["name"],
239
+ "prefix": row["prefix"],
240
+ "scopes": json.loads(row["scopes"]),
241
+ "created_at": self._ensure_rfc3339(row["created_at"]),
242
+ "last_used_at": self._ensure_rfc3339(row["last_used_at"]) if row["last_used_at"] else None,
243
+ "expires_at": self._ensure_rfc3339(row["expires_at"]) if row["expires_at"] else None,
244
+ "is_active": bool(row["is_active"]),
245
+ }
@@ -0,0 +1,67 @@
1
+ """Repository for Audit Repository operations.
2
+
3
+ Extracted from monolithic Database class for better maintainability.
4
+ """
5
+
6
+ import json
7
+ from datetime import datetime
8
+ from typing import Optional, Dict, Any
9
+ import logging
10
+
11
+
12
+ from codeframe.platform_store.repositories.base import BaseRepository
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class AuditRepository(BaseRepository):
18
+ """Repository for audit repository operations."""
19
+
20
+
21
+ def create_audit_log(
22
+ self,
23
+ event_type: str,
24
+ user_id: Optional[int],
25
+ resource_type: str,
26
+ resource_id: Optional[int],
27
+ ip_address: Optional[str],
28
+ metadata: Optional[Dict[str, Any]],
29
+ timestamp: datetime,
30
+ ) -> int:
31
+ """Create an audit log entry (Issue #132).
32
+
33
+ Args:
34
+ event_type: Type of event (e.g., "auth.login.success")
35
+ user_id: User ID (if authenticated)
36
+ resource_type: Type of resource (e.g., "project", "task")
37
+ resource_id: ID of the resource
38
+ ip_address: Client IP address
39
+ metadata: Additional event metadata (stored as JSON)
40
+ timestamp: Event timestamp
41
+
42
+ Returns:
43
+ ID of the created audit log entry
44
+ """
45
+
46
+ cursor = self.conn.cursor()
47
+ cursor.execute(
48
+ """
49
+ INSERT INTO audit_logs (
50
+ event_type, user_id, resource_type, resource_id,
51
+ ip_address, metadata, timestamp
52
+ )
53
+ VALUES (?, ?, ?, ?, ?, ?, ?)
54
+ """,
55
+ (
56
+ event_type,
57
+ user_id,
58
+ resource_type,
59
+ resource_id,
60
+ ip_address,
61
+ json.dumps(metadata) if metadata else None,
62
+ timestamp.isoformat(),
63
+ ),
64
+ )
65
+ self.conn.commit()
66
+ return cursor.lastrowid
67
+