marvisx-cli 0.1.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 (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,253 @@
1
+ # v2.0.0 - 2026-05-27 - S1 F1.1: thin adapter over use_cases.learnings (TEMPLATE router)
2
+ # v1.1.0 - 2026-04-16 - Phase 7.0 KG Inline Lens: ?deep=true on get/list/check + H4 visibility guard
3
+ """HTTP adapter for the learnings domain (S1 collapse-runtime TEMPLATE).
4
+
5
+ This router is a thin transport adapter. All CRUD/search/validation/RBAC/
6
+ visibility logic lives in :mod:`core.api.use_cases.learnings` (pure, fastapi-free).
7
+ Each handler:
8
+
9
+ 1. resolves identity into a :class:`CallerContext` (``from_user_info``; learnings
10
+ has no human-only gate, so ``is_human_session=False`` — no ``Request`` needed);
11
+ 2. for ``list``, resolves visibility at the boundary (DECISION 1) and passes it in;
12
+ 3. calls the use_case inside ``try/except ServiceError`` -> ``to_http``;
13
+ 4. owns everything ``deep``-related: the ``deep_requires_filter`` 400 guard
14
+ (DECISION 3) and the rate-limit + audit + ``kg_context`` enrichment (DECISION 2).
15
+
16
+ The pure helpers and DTOs are re-exported from the use_case so existing importers
17
+ (``main.py`` for ``router``; ``services``/tests for the helpers) keep working.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import logging
23
+
24
+ import aiosqlite
25
+ from fastapi import APIRouter, Depends, HTTPException, Query
26
+ from pydantic import BaseModel, Field
27
+
28
+ from core.api.db import get_db, get_write_db
29
+ from core.api.models import UserInfo
30
+ from core.api.rbac import require_role
31
+ from core.api.routers._adapter import to_http
32
+ from core.api.security import get_current_user_or_agent
33
+ from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
34
+ from core.api.services.kg.lens import build_kg_context_for_learning
35
+ from core.api.use_cases import learnings as uc
36
+ from core.api.use_cases._context import CallerContext
37
+ from core.api.use_cases._errors import ServiceError
38
+ from core.api.visibility import get_visible_projects
39
+
40
+ # Re-export the domain DTOs + pure helpers + constants from the use_case so that
41
+ # (a) `response_model=` below references the same classes and (b) existing
42
+ # importers — `services/kg/lens.py`, `core/api/tests/test_learnings_check_search.py`
43
+ # — keep importing them from this router path unchanged.
44
+ from core.api.use_cases.learnings import ( # noqa: F401 (re-export surface)
45
+ CHECK_LEARNINGS_STOPWORDS,
46
+ VALID_CATEGORIES,
47
+ VALID_SEVERITIES,
48
+ LearningCheckResponse,
49
+ LearningResponse,
50
+ _extract_check_terms,
51
+ _learning_match_score,
52
+ _row_to_learning,
53
+ _search_learning_rows,
54
+ )
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ router = APIRouter(prefix="/api/v1/learnings", tags=["learnings"])
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # HTTP request models (transport contract — these STAY in the router)
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ class LearningCreateRequest(BaseModel):
67
+ title: str
68
+ category: str
69
+ description: str
70
+ tags: list[str] = Field(default_factory=list)
71
+ module: str | None = None
72
+ severity: str = "medium"
73
+ prevention: str | None = None
74
+ session: int | None = None
75
+ project: str | None = None
76
+
77
+
78
+ class LearningUpdateRequest(BaseModel):
79
+ title: str | None = None
80
+ category: str | None = None
81
+ description: str | None = None
82
+ tags: list[str] | None = None
83
+ module: str | None = None
84
+ severity: str | None = None
85
+ prevention: str | None = None
86
+ session: int | None = None
87
+ project: str | None = None
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Endpoints (thin adapters)
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ @router.get("/check", response_model=LearningCheckResponse)
96
+ async def check_learnings(
97
+ q: str = Query(
98
+ ..., description="Keyword to search in title, description, tags, module"
99
+ ),
100
+ module: str | None = Query(
101
+ None, description="Filter by module (exact or LIKE match)"
102
+ ),
103
+ deep: bool = Query(False, description="Attach KG context to each result (Phase 7.0)"),
104
+ user: UserInfo = Depends(get_current_user_or_agent),
105
+ db: aiosqlite.Connection = Depends(get_db),
106
+ ) -> LearningCheckResponse:
107
+ """Search learnings relevant to a keyword/module. Used by MCP before risky actions."""
108
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
109
+ try:
110
+ result = await uc.check_learnings(ctx, db, query=q, module=module)
111
+ except ServiceError as e:
112
+ raise to_http(e)
113
+
114
+ # DECISION 2: deep KG enrichment is a per-surface adapter concern.
115
+ if deep and result.results:
116
+ check_deep_rate_limit(user.username)
117
+ log_kg_deep_access(user.username, "check_learnings", q)
118
+ kg_contexts = await asyncio.gather(
119
+ *[build_kg_context_for_learning(db, r.id, deep=True) for r in result.results],
120
+ return_exceptions=True,
121
+ )
122
+ for r, kg_ctx in zip(result.results, kg_contexts):
123
+ if not isinstance(kg_ctx, Exception):
124
+ r.kg_context = kg_ctx
125
+
126
+ return result
127
+
128
+
129
+ @router.get("", response_model=list[LearningResponse])
130
+ async def list_learnings(
131
+ category: str | None = None,
132
+ project: str | None = None,
133
+ severity: str | None = None,
134
+ tags: str | None = Query(None, description="Comma-separated tags (OR logic)"),
135
+ search: str | None = Query(None, description="Free text search"),
136
+ module: str | None = Query(None, description="Filter by module"),
137
+ limit: int = Query(50, ge=1, le=200),
138
+ offset: int = Query(0, ge=0),
139
+ deep: bool = Query(False, description="Attach KG context (aggregate deferred to Phase 7.1)"),
140
+ user: UserInfo = Depends(get_current_user_or_agent),
141
+ db: aiosqlite.Connection = Depends(get_db),
142
+ ) -> list[LearningResponse]:
143
+ """List learnings with filters."""
144
+ # DECISION 3: deep_requires_filter is an adapter-owned guard tied to the
145
+ # adapter-owned `deep` feature. Keep the exact 400 (NOT via ServiceError).
146
+ if deep and not project and not module and not search:
147
+ raise HTTPException(
148
+ status_code=400,
149
+ detail="deep=true requires ?project=, ?module=, or ?search= filter",
150
+ )
151
+
152
+ # DECISION 1 (the visibility template): resolve visibility at the boundary
153
+ # (needs UserInfo.teams, not carried by CallerContext) and pass it in; the
154
+ # use_case enforces it. Only resolve when a project filter is present.
155
+ visible_projects = await get_visible_projects(db, user) if project else None
156
+
157
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
158
+ try:
159
+ return await uc.list_learnings(
160
+ ctx,
161
+ db,
162
+ category=category,
163
+ project=project,
164
+ severity=severity,
165
+ tags=tags,
166
+ search=search,
167
+ module=module,
168
+ limit=limit,
169
+ offset=offset,
170
+ visible_projects=visible_projects,
171
+ )
172
+ except ServiceError as e:
173
+ raise to_http(e)
174
+
175
+
176
+ @router.get("/{learning_id}", response_model=LearningResponse)
177
+ async def get_learning(
178
+ learning_id: str,
179
+ deep: bool = Query(False, description="Attach KG context (Phase 7.0)"),
180
+ user: UserInfo = Depends(get_current_user_or_agent),
181
+ db: aiosqlite.Connection = Depends(get_db),
182
+ ) -> LearningResponse:
183
+ """Get a single learning by ID."""
184
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
185
+ try:
186
+ result = await uc.get_learning(ctx, db, learning_id=learning_id)
187
+ except ServiceError as e:
188
+ raise to_http(e)
189
+
190
+ # DECISION 2: deep KG enrichment is a per-surface adapter concern.
191
+ if deep:
192
+ check_deep_rate_limit(user.username)
193
+ log_kg_deep_access(user.username, "get_learning", learning_id)
194
+ result.kg_context = await build_kg_context_for_learning(db, learning_id, deep=True)
195
+ return result
196
+
197
+
198
+ @router.post("", response_model=LearningResponse, status_code=201)
199
+ async def create_learning(
200
+ body: LearningCreateRequest,
201
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
202
+ db: aiosqlite.Connection = Depends(get_write_db),
203
+ ) -> LearningResponse:
204
+ """Create a new learning."""
205
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
206
+ try:
207
+ return await uc.create_learning(
208
+ ctx,
209
+ db,
210
+ title=body.title,
211
+ category=body.category,
212
+ description=body.description,
213
+ tags=body.tags,
214
+ module=body.module,
215
+ severity=body.severity,
216
+ prevention=body.prevention,
217
+ session=body.session,
218
+ project=body.project,
219
+ )
220
+ except ServiceError as e:
221
+ raise to_http(e)
222
+
223
+
224
+ @router.patch("/{learning_id}", response_model=LearningResponse)
225
+ async def update_learning(
226
+ learning_id: str,
227
+ body: LearningUpdateRequest,
228
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
229
+ db: aiosqlite.Connection = Depends(get_write_db),
230
+ ) -> LearningResponse:
231
+ """Update a learning."""
232
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
233
+ # exclude_unset preserves the HTTP semantic (omitted = unchanged, null = clear),
234
+ # equivalent to the previous `body.model_fields_set` gating.
235
+ fields = body.model_dump(exclude_unset=True)
236
+ try:
237
+ return await uc.update_learning(ctx, db, learning_id=learning_id, fields=fields)
238
+ except ServiceError as e:
239
+ raise to_http(e)
240
+
241
+
242
+ @router.post("/{learning_id}/bump", response_model=LearningResponse)
243
+ async def bump_learning(
244
+ learning_id: str,
245
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
246
+ db: aiosqlite.Connection = Depends(get_write_db),
247
+ ) -> LearningResponse:
248
+ """Increment frequency and update last_occurrence. Used when the same issue recurs."""
249
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
250
+ try:
251
+ return await uc.bump_learning(ctx, db, learning_id=learning_id)
252
+ except ServiceError as e:
253
+ raise to_http(e)
@@ -0,0 +1,130 @@
1
+ # v1.0.0 - 2026-05-26 - M1 CAPTURE U5 — per-function LLM config + provider keys (admin)
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ import aiosqlite
7
+ from fastapi import APIRouter, Depends, HTTPException, Path as PathParam
8
+
9
+ from core.api.db import get_db, get_write_db
10
+ from core.api.models.llm_config import (
11
+ LLMFunction,
12
+ LLMFunctionConfigItem,
13
+ LLMFunctionConfigUpdate,
14
+ ProviderKeyCreateRequest,
15
+ ProviderKeyResponse,
16
+ )
17
+ from core.api.rbac import require_role
18
+ from core.api.services.ingest.llm import config_store
19
+
20
+ logger = logging.getLogger(__name__)
21
+ router = APIRouter(prefix="/api/v1/llm-config", tags=["llm-config"])
22
+
23
+ _FUNCTIONS = ("classify", "embedding", "brain")
24
+
25
+
26
+ def _ws(caller) -> str:
27
+ return getattr(caller, "workspace_id", None) or "ws_default"
28
+
29
+
30
+ @router.get("/status")
31
+ async def get_llm_status(
32
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
33
+ db: aiosqlite.Connection = Depends(get_db),
34
+ ) -> dict[str, str]:
35
+ """Auto-classify provider status, drives the Console 'configure a provider'
36
+ alert: 'configured' or 'disabled_no_provider'."""
37
+ return {"classify": await config_store.classify_provider_status(db, _ws(caller))}
38
+
39
+
40
+ @router.get("", response_model=list[LLMFunctionConfigItem])
41
+ async def get_llm_config(
42
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
43
+ db: aiosqlite.Connection = Depends(get_db),
44
+ ) -> list[LLMFunctionConfigItem]:
45
+ rows = await config_store.list_function_configs(db, _ws(caller))
46
+ return [LLMFunctionConfigItem(**row) for row in rows]
47
+
48
+
49
+ @router.put("/{function_name}", response_model=LLMFunctionConfigItem)
50
+ async def put_llm_config(
51
+ body: LLMFunctionConfigUpdate,
52
+ function_name: LLMFunction = PathParam(...),
53
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
54
+ db: aiosqlite.Connection = Depends(get_write_db),
55
+ ) -> LLMFunctionConfigItem:
56
+ if function_name not in _FUNCTIONS:
57
+ raise HTTPException(status_code=422, detail=f"unknown function {function_name!r}")
58
+ ws = _ws(caller)
59
+ if body.provider_key_id is not None:
60
+ async with db.execute(
61
+ "SELECT 1 FROM provider_keys WHERE id = ? AND workspace_id = ?",
62
+ (body.provider_key_id, ws),
63
+ ) as cur:
64
+ if await cur.fetchone() is None:
65
+ raise HTTPException(status_code=404, detail="provider_key_id not found")
66
+ await config_store.set_function_config(
67
+ db,
68
+ function_name=function_name,
69
+ provider_key_id=body.provider_key_id,
70
+ model=body.model,
71
+ enabled=body.enabled,
72
+ workspace_id=ws,
73
+ )
74
+ await db.commit()
75
+ rows = await config_store.list_function_configs(db, ws)
76
+ item = next(r for r in rows if r["function_name"] == function_name)
77
+ return LLMFunctionConfigItem(**item)
78
+
79
+
80
+ @router.get("/provider-keys", response_model=list[ProviderKeyResponse])
81
+ async def list_provider_keys(
82
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
83
+ db: aiosqlite.Connection = Depends(get_db),
84
+ ) -> list[ProviderKeyResponse]:
85
+ rows = await config_store.list_provider_keys(db, _ws(caller))
86
+ return [ProviderKeyResponse(**row) for row in rows]
87
+
88
+
89
+ @router.post("/provider-keys", response_model=ProviderKeyResponse, status_code=201)
90
+ async def create_provider_key(
91
+ body: ProviderKeyCreateRequest,
92
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
93
+ db: aiosqlite.Connection = Depends(get_write_db),
94
+ ) -> ProviderKeyResponse:
95
+ ws = _ws(caller)
96
+ try:
97
+ key_id = await config_store.create_provider_key(
98
+ db,
99
+ provider=body.provider,
100
+ label=body.label,
101
+ api_key=body.api_key,
102
+ base_url=body.base_url,
103
+ workspace_id=ws,
104
+ )
105
+ except ValueError as exc:
106
+ raise HTTPException(status_code=422, detail=str(exc))
107
+ except RuntimeError:
108
+ # crypto fail-closed (BYOK_FERNET_SECRET missing) — never echo internals.
109
+ logger.error("create_provider_key: encryption unavailable", exc_info=True)
110
+ raise HTTPException(
111
+ status_code=503,
112
+ detail="Provider key storage unavailable: server encryption is not configured.",
113
+ )
114
+ await db.commit()
115
+ rows = await config_store.list_provider_keys(db, ws)
116
+ row = next(r for r in rows if r["id"] == key_id)
117
+ return ProviderKeyResponse(**row)
118
+
119
+
120
+ @router.delete("/provider-keys/{key_id}", status_code=204)
121
+ async def delete_provider_key(
122
+ key_id: str,
123
+ caller=Depends(require_role("admin", "super_admin", human_only=True)),
124
+ db: aiosqlite.Connection = Depends(get_write_db),
125
+ ) -> None:
126
+ ws = _ws(caller)
127
+ deleted = await config_store.delete_provider_key(db, key_id, ws)
128
+ await db.commit()
129
+ if not deleted:
130
+ raise HTTPException(status_code=404, detail="provider key not found")