provekit 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 (485) hide show
  1. provekit/__init__.py +0 -0
  2. provekit/cli.py +256 -0
  3. provekit/config.py +77 -0
  4. provekit/database.py +132 -0
  5. provekit/main.py +115 -0
  6. provekit/migrations/env.py +40 -0
  7. provekit/migrations/script.py.mako +22 -0
  8. provekit/migrations/versions/323eb73d463c_user_email_verified.py +32 -0
  9. provekit/migrations/versions/49e8ab812556_baseline_schema.py +241 -0
  10. provekit/migrations/versions/819ed5ff183e_workspace_ingest_key.py +35 -0
  11. provekit/migrations/versions/a1b2c3d4e5f6_user_token_version.py +27 -0
  12. provekit/models.py +191 -0
  13. provekit/observability.py +154 -0
  14. provekit/routers/__init__.py +0 -0
  15. provekit/routers/auth.py +150 -0
  16. provekit/routers/connections.py +270 -0
  17. provekit/routers/deployments.py +161 -0
  18. provekit/routers/flows.py +188 -0
  19. provekit/routers/library.py +265 -0
  20. provekit/routers/prompts.py +72 -0
  21. provekit/routers/run.py +203 -0
  22. provekit/routers/runtime.py +103 -0
  23. provekit/routers/traces.py +65 -0
  24. provekit/routers/usage.py +42 -0
  25. provekit/services/__init__.py +0 -0
  26. provekit/services/assertions.py +135 -0
  27. provekit/services/auth.py +131 -0
  28. provekit/services/deploy.py +63 -0
  29. provekit/services/dispatch.py +313 -0
  30. provekit/services/email.py +38 -0
  31. provekit/services/flow.py +299 -0
  32. provekit/services/limits.py +109 -0
  33. provekit/services/masking.py +45 -0
  34. provekit/services/netguard.py +69 -0
  35. provekit/services/otel.py +146 -0
  36. provekit/services/promptfoo.py +92 -0
  37. provekit/services/providers/__init__.py +0 -0
  38. provekit/services/providers/a2a_client.py +110 -0
  39. provekit/services/providers/agent_http.py +55 -0
  40. provekit/services/providers/llm.py +364 -0
  41. provekit/services/providers/mcp_client.py +289 -0
  42. provekit/services/runstore.py +93 -0
  43. provekit/services/sealing.py +106 -0
  44. provekit/services/templates.py +69 -0
  45. provekit/services/testfile.py +122 -0
  46. provekit/services/tooling.py +194 -0
  47. provekit/services/workspace.py +91 -0
  48. provekit/templates/flows/_manifest.json +2594 -0
  49. provekit/templates/flows/banking-action-items.yaml +43 -0
  50. provekit/templates/flows/banking-answer.yaml +43 -0
  51. provekit/templates/flows/banking-classify-intent.yaml +43 -0
  52. provekit/templates/flows/banking-draft-critique-revise.yaml +75 -0
  53. provekit/templates/flows/banking-extract-to-json.yaml +43 -0
  54. provekit/templates/flows/banking-faq-match-answer.yaml +60 -0
  55. provekit/templates/flows/banking-keyword-extract.yaml +43 -0
  56. provekit/templates/flows/banking-language-route.yaml +103 -0
  57. provekit/templates/flows/banking-moderation-gate.yaml +73 -0
  58. provekit/templates/flows/banking-redact-summarize.yaml +59 -0
  59. provekit/templates/flows/banking-score-gate.yaml +73 -0
  60. provekit/templates/flows/banking-sentiment-route.yaml +73 -0
  61. provekit/templates/flows/banking-severity-tiers.yaml +103 -0
  62. provekit/templates/flows/banking-summarize-then-decide.yaml +88 -0
  63. provekit/templates/flows/banking-tag-enrich.yaml +43 -0
  64. provekit/templates/flows/banking-tone-rewrite.yaml +43 -0
  65. provekit/templates/flows/banking-translate-reply.yaml +58 -0
  66. provekit/templates/flows/banking-triage-route.yaml +73 -0
  67. provekit/templates/flows/community-forum-action-items.yaml +43 -0
  68. provekit/templates/flows/community-forum-answer.yaml +43 -0
  69. provekit/templates/flows/community-forum-classify-intent.yaml +43 -0
  70. provekit/templates/flows/community-forum-draft-critique-revise.yaml +75 -0
  71. provekit/templates/flows/community-forum-extract-to-json.yaml +43 -0
  72. provekit/templates/flows/community-forum-faq-match-answer.yaml +60 -0
  73. provekit/templates/flows/community-forum-keyword-extract.yaml +43 -0
  74. provekit/templates/flows/community-forum-language-route.yaml +103 -0
  75. provekit/templates/flows/community-forum-moderation-gate.yaml +73 -0
  76. provekit/templates/flows/community-forum-redact-summarize.yaml +59 -0
  77. provekit/templates/flows/community-forum-score-gate.yaml +73 -0
  78. provekit/templates/flows/community-forum-sentiment-route.yaml +73 -0
  79. provekit/templates/flows/community-forum-severity-tiers.yaml +103 -0
  80. provekit/templates/flows/community-forum-summarize-then-decide.yaml +88 -0
  81. provekit/templates/flows/community-forum-tag-enrich.yaml +43 -0
  82. provekit/templates/flows/community-forum-tone-rewrite.yaml +43 -0
  83. provekit/templates/flows/community-forum-translate-reply.yaml +58 -0
  84. provekit/templates/flows/community-forum-triage-route.yaml +73 -0
  85. provekit/templates/flows/content-moderation-action-items.yaml +43 -0
  86. provekit/templates/flows/content-moderation-answer.yaml +43 -0
  87. provekit/templates/flows/content-moderation-classify-intent.yaml +43 -0
  88. provekit/templates/flows/content-moderation-draft-critique-revise.yaml +75 -0
  89. provekit/templates/flows/content-moderation-extract-to-json.yaml +43 -0
  90. provekit/templates/flows/content-moderation-faq-match-answer.yaml +60 -0
  91. provekit/templates/flows/content-moderation-keyword-extract.yaml +43 -0
  92. provekit/templates/flows/content-moderation-language-route.yaml +103 -0
  93. provekit/templates/flows/content-moderation-moderation-gate.yaml +73 -0
  94. provekit/templates/flows/content-moderation-redact-summarize.yaml +59 -0
  95. provekit/templates/flows/content-moderation-score-gate.yaml +73 -0
  96. provekit/templates/flows/content-moderation-sentiment-route.yaml +73 -0
  97. provekit/templates/flows/content-moderation-severity-tiers.yaml +103 -0
  98. provekit/templates/flows/content-moderation-summarize-then-decide.yaml +88 -0
  99. provekit/templates/flows/content-moderation-tag-enrich.yaml +43 -0
  100. provekit/templates/flows/content-moderation-tone-rewrite.yaml +43 -0
  101. provekit/templates/flows/content-moderation-translate-reply.yaml +58 -0
  102. provekit/templates/flows/content-moderation-triage-route.yaml +73 -0
  103. provekit/templates/flows/customer-support-action-items.yaml +43 -0
  104. provekit/templates/flows/customer-support-answer.yaml +43 -0
  105. provekit/templates/flows/customer-support-classify-intent.yaml +43 -0
  106. provekit/templates/flows/customer-support-draft-critique-revise.yaml +75 -0
  107. provekit/templates/flows/customer-support-extract-to-json.yaml +43 -0
  108. provekit/templates/flows/customer-support-faq-match-answer.yaml +60 -0
  109. provekit/templates/flows/customer-support-keyword-extract.yaml +43 -0
  110. provekit/templates/flows/customer-support-language-route.yaml +103 -0
  111. provekit/templates/flows/customer-support-moderation-gate.yaml +73 -0
  112. provekit/templates/flows/customer-support-redact-summarize.yaml +59 -0
  113. provekit/templates/flows/customer-support-score-gate.yaml +73 -0
  114. provekit/templates/flows/customer-support-sentiment-route.yaml +73 -0
  115. provekit/templates/flows/customer-support-severity-tiers.yaml +103 -0
  116. provekit/templates/flows/customer-support-summarize-then-decide.yaml +88 -0
  117. provekit/templates/flows/customer-support-tag-enrich.yaml +43 -0
  118. provekit/templates/flows/customer-support-tone-rewrite.yaml +43 -0
  119. provekit/templates/flows/customer-support-translate-reply.yaml +58 -0
  120. provekit/templates/flows/customer-support-triage-route.yaml +73 -0
  121. provekit/templates/flows/developer-tooling-action-items.yaml +43 -0
  122. provekit/templates/flows/developer-tooling-answer.yaml +43 -0
  123. provekit/templates/flows/developer-tooling-classify-intent.yaml +43 -0
  124. provekit/templates/flows/developer-tooling-draft-critique-revise.yaml +75 -0
  125. provekit/templates/flows/developer-tooling-extract-to-json.yaml +43 -0
  126. provekit/templates/flows/developer-tooling-faq-match-answer.yaml +60 -0
  127. provekit/templates/flows/developer-tooling-keyword-extract.yaml +43 -0
  128. provekit/templates/flows/developer-tooling-language-route.yaml +103 -0
  129. provekit/templates/flows/developer-tooling-moderation-gate.yaml +73 -0
  130. provekit/templates/flows/developer-tooling-redact-summarize.yaml +59 -0
  131. provekit/templates/flows/developer-tooling-score-gate.yaml +73 -0
  132. provekit/templates/flows/developer-tooling-sentiment-route.yaml +73 -0
  133. provekit/templates/flows/developer-tooling-severity-tiers.yaml +103 -0
  134. provekit/templates/flows/developer-tooling-summarize-then-decide.yaml +88 -0
  135. provekit/templates/flows/developer-tooling-tag-enrich.yaml +43 -0
  136. provekit/templates/flows/developer-tooling-tone-rewrite.yaml +43 -0
  137. provekit/templates/flows/developer-tooling-translate-reply.yaml +58 -0
  138. provekit/templates/flows/developer-tooling-triage-route.yaml +73 -0
  139. provekit/templates/flows/devrel-action-items.yaml +43 -0
  140. provekit/templates/flows/devrel-answer.yaml +43 -0
  141. provekit/templates/flows/devrel-classify-intent.yaml +43 -0
  142. provekit/templates/flows/devrel-draft-critique-revise.yaml +75 -0
  143. provekit/templates/flows/devrel-extract-to-json.yaml +43 -0
  144. provekit/templates/flows/devrel-faq-match-answer.yaml +60 -0
  145. provekit/templates/flows/devrel-keyword-extract.yaml +43 -0
  146. provekit/templates/flows/devrel-language-route.yaml +103 -0
  147. provekit/templates/flows/devrel-moderation-gate.yaml +73 -0
  148. provekit/templates/flows/devrel-redact-summarize.yaml +59 -0
  149. provekit/templates/flows/devrel-score-gate.yaml +73 -0
  150. provekit/templates/flows/devrel-sentiment-route.yaml +73 -0
  151. provekit/templates/flows/devrel-severity-tiers.yaml +103 -0
  152. provekit/templates/flows/devrel-summarize-then-decide.yaml +88 -0
  153. provekit/templates/flows/devrel-tag-enrich.yaml +43 -0
  154. provekit/templates/flows/devrel-tone-rewrite.yaml +43 -0
  155. provekit/templates/flows/devrel-translate-reply.yaml +58 -0
  156. provekit/templates/flows/devrel-triage-route.yaml +73 -0
  157. provekit/templates/flows/e-commerce-action-items.yaml +43 -0
  158. provekit/templates/flows/e-commerce-answer.yaml +43 -0
  159. provekit/templates/flows/e-commerce-classify-intent.yaml +43 -0
  160. provekit/templates/flows/e-commerce-draft-critique-revise.yaml +75 -0
  161. provekit/templates/flows/e-commerce-extract-to-json.yaml +43 -0
  162. provekit/templates/flows/e-commerce-faq-match-answer.yaml +60 -0
  163. provekit/templates/flows/e-commerce-keyword-extract.yaml +43 -0
  164. provekit/templates/flows/e-commerce-language-route.yaml +103 -0
  165. provekit/templates/flows/e-commerce-moderation-gate.yaml +73 -0
  166. provekit/templates/flows/e-commerce-redact-summarize.yaml +59 -0
  167. provekit/templates/flows/e-commerce-score-gate.yaml +73 -0
  168. provekit/templates/flows/e-commerce-sentiment-route.yaml +73 -0
  169. provekit/templates/flows/e-commerce-severity-tiers.yaml +103 -0
  170. provekit/templates/flows/e-commerce-summarize-then-decide.yaml +88 -0
  171. provekit/templates/flows/e-commerce-tag-enrich.yaml +43 -0
  172. provekit/templates/flows/e-commerce-tone-rewrite.yaml +43 -0
  173. provekit/templates/flows/e-commerce-translate-reply.yaml +58 -0
  174. provekit/templates/flows/e-commerce-triage-route.yaml +73 -0
  175. provekit/templates/flows/education-action-items.yaml +43 -0
  176. provekit/templates/flows/education-answer.yaml +43 -0
  177. provekit/templates/flows/education-classify-intent.yaml +43 -0
  178. provekit/templates/flows/education-draft-critique-revise.yaml +75 -0
  179. provekit/templates/flows/education-extract-to-json.yaml +43 -0
  180. provekit/templates/flows/education-faq-match-answer.yaml +60 -0
  181. provekit/templates/flows/education-keyword-extract.yaml +43 -0
  182. provekit/templates/flows/education-language-route.yaml +103 -0
  183. provekit/templates/flows/education-moderation-gate.yaml +73 -0
  184. provekit/templates/flows/education-redact-summarize.yaml +59 -0
  185. provekit/templates/flows/education-score-gate.yaml +73 -0
  186. provekit/templates/flows/education-sentiment-route.yaml +73 -0
  187. provekit/templates/flows/education-severity-tiers.yaml +103 -0
  188. provekit/templates/flows/education-summarize-then-decide.yaml +88 -0
  189. provekit/templates/flows/education-tag-enrich.yaml +43 -0
  190. provekit/templates/flows/education-tone-rewrite.yaml +43 -0
  191. provekit/templates/flows/education-translate-reply.yaml +58 -0
  192. provekit/templates/flows/education-triage-route.yaml +73 -0
  193. provekit/templates/flows/finance-action-items.yaml +43 -0
  194. provekit/templates/flows/finance-answer.yaml +43 -0
  195. provekit/templates/flows/finance-classify-intent.yaml +43 -0
  196. provekit/templates/flows/finance-draft-critique-revise.yaml +75 -0
  197. provekit/templates/flows/finance-extract-to-json.yaml +43 -0
  198. provekit/templates/flows/finance-faq-match-answer.yaml +60 -0
  199. provekit/templates/flows/finance-keyword-extract.yaml +43 -0
  200. provekit/templates/flows/finance-language-route.yaml +103 -0
  201. provekit/templates/flows/finance-moderation-gate.yaml +73 -0
  202. provekit/templates/flows/finance-redact-summarize.yaml +59 -0
  203. provekit/templates/flows/finance-score-gate.yaml +73 -0
  204. provekit/templates/flows/finance-sentiment-route.yaml +73 -0
  205. provekit/templates/flows/finance-severity-tiers.yaml +103 -0
  206. provekit/templates/flows/finance-summarize-then-decide.yaml +88 -0
  207. provekit/templates/flows/finance-tag-enrich.yaml +43 -0
  208. provekit/templates/flows/finance-tone-rewrite.yaml +43 -0
  209. provekit/templates/flows/finance-translate-reply.yaml +58 -0
  210. provekit/templates/flows/finance-triage-route.yaml +73 -0
  211. provekit/templates/flows/food-delivery-action-items.yaml +43 -0
  212. provekit/templates/flows/food-delivery-answer.yaml +43 -0
  213. provekit/templates/flows/food-delivery-classify-intent.yaml +43 -0
  214. provekit/templates/flows/food-delivery-draft-critique-revise.yaml +75 -0
  215. provekit/templates/flows/food-delivery-extract-to-json.yaml +43 -0
  216. provekit/templates/flows/food-delivery-faq-match-answer.yaml +60 -0
  217. provekit/templates/flows/food-delivery-keyword-extract.yaml +43 -0
  218. provekit/templates/flows/food-delivery-language-route.yaml +103 -0
  219. provekit/templates/flows/food-delivery-moderation-gate.yaml +73 -0
  220. provekit/templates/flows/food-delivery-redact-summarize.yaml +59 -0
  221. provekit/templates/flows/food-delivery-score-gate.yaml +73 -0
  222. provekit/templates/flows/food-delivery-sentiment-route.yaml +73 -0
  223. provekit/templates/flows/food-delivery-severity-tiers.yaml +103 -0
  224. provekit/templates/flows/food-delivery-summarize-then-decide.yaml +88 -0
  225. provekit/templates/flows/food-delivery-tag-enrich.yaml +43 -0
  226. provekit/templates/flows/food-delivery-tone-rewrite.yaml +43 -0
  227. provekit/templates/flows/food-delivery-translate-reply.yaml +58 -0
  228. provekit/templates/flows/food-delivery-triage-route.yaml +73 -0
  229. provekit/templates/flows/gaming-support-action-items.yaml +43 -0
  230. provekit/templates/flows/gaming-support-answer.yaml +43 -0
  231. provekit/templates/flows/gaming-support-classify-intent.yaml +43 -0
  232. provekit/templates/flows/gaming-support-draft-critique-revise.yaml +75 -0
  233. provekit/templates/flows/gaming-support-extract-to-json.yaml +43 -0
  234. provekit/templates/flows/gaming-support-faq-match-answer.yaml +60 -0
  235. provekit/templates/flows/gaming-support-keyword-extract.yaml +43 -0
  236. provekit/templates/flows/gaming-support-language-route.yaml +103 -0
  237. provekit/templates/flows/gaming-support-moderation-gate.yaml +73 -0
  238. provekit/templates/flows/gaming-support-redact-summarize.yaml +59 -0
  239. provekit/templates/flows/gaming-support-score-gate.yaml +73 -0
  240. provekit/templates/flows/gaming-support-sentiment-route.yaml +73 -0
  241. provekit/templates/flows/gaming-support-severity-tiers.yaml +103 -0
  242. provekit/templates/flows/gaming-support-summarize-then-decide.yaml +88 -0
  243. provekit/templates/flows/gaming-support-tag-enrich.yaml +43 -0
  244. provekit/templates/flows/gaming-support-tone-rewrite.yaml +43 -0
  245. provekit/templates/flows/gaming-support-translate-reply.yaml +58 -0
  246. provekit/templates/flows/gaming-support-triage-route.yaml +73 -0
  247. provekit/templates/flows/healthcare-intake-action-items.yaml +43 -0
  248. provekit/templates/flows/healthcare-intake-answer.yaml +43 -0
  249. provekit/templates/flows/healthcare-intake-classify-intent.yaml +43 -0
  250. provekit/templates/flows/healthcare-intake-draft-critique-revise.yaml +75 -0
  251. provekit/templates/flows/healthcare-intake-extract-to-json.yaml +43 -0
  252. provekit/templates/flows/healthcare-intake-faq-match-answer.yaml +60 -0
  253. provekit/templates/flows/healthcare-intake-keyword-extract.yaml +43 -0
  254. provekit/templates/flows/healthcare-intake-language-route.yaml +103 -0
  255. provekit/templates/flows/healthcare-intake-moderation-gate.yaml +73 -0
  256. provekit/templates/flows/healthcare-intake-redact-summarize.yaml +59 -0
  257. provekit/templates/flows/healthcare-intake-score-gate.yaml +73 -0
  258. provekit/templates/flows/healthcare-intake-sentiment-route.yaml +73 -0
  259. provekit/templates/flows/healthcare-intake-severity-tiers.yaml +103 -0
  260. provekit/templates/flows/healthcare-intake-summarize-then-decide.yaml +88 -0
  261. provekit/templates/flows/healthcare-intake-tag-enrich.yaml +43 -0
  262. provekit/templates/flows/healthcare-intake-tone-rewrite.yaml +43 -0
  263. provekit/templates/flows/healthcare-intake-translate-reply.yaml +58 -0
  264. provekit/templates/flows/healthcare-intake-triage-route.yaml +73 -0
  265. provekit/templates/flows/hr-action-items.yaml +43 -0
  266. provekit/templates/flows/hr-answer.yaml +43 -0
  267. provekit/templates/flows/hr-classify-intent.yaml +43 -0
  268. provekit/templates/flows/hr-draft-critique-revise.yaml +75 -0
  269. provekit/templates/flows/hr-extract-to-json.yaml +43 -0
  270. provekit/templates/flows/hr-faq-match-answer.yaml +60 -0
  271. provekit/templates/flows/hr-keyword-extract.yaml +43 -0
  272. provekit/templates/flows/hr-language-route.yaml +103 -0
  273. provekit/templates/flows/hr-moderation-gate.yaml +73 -0
  274. provekit/templates/flows/hr-redact-summarize.yaml +59 -0
  275. provekit/templates/flows/hr-score-gate.yaml +73 -0
  276. provekit/templates/flows/hr-sentiment-route.yaml +73 -0
  277. provekit/templates/flows/hr-severity-tiers.yaml +103 -0
  278. provekit/templates/flows/hr-summarize-then-decide.yaml +88 -0
  279. provekit/templates/flows/hr-tag-enrich.yaml +43 -0
  280. provekit/templates/flows/hr-tone-rewrite.yaml +43 -0
  281. provekit/templates/flows/hr-translate-reply.yaml +58 -0
  282. provekit/templates/flows/hr-triage-route.yaml +73 -0
  283. provekit/templates/flows/insurance-action-items.yaml +43 -0
  284. provekit/templates/flows/insurance-answer.yaml +43 -0
  285. provekit/templates/flows/insurance-classify-intent.yaml +43 -0
  286. provekit/templates/flows/insurance-draft-critique-revise.yaml +75 -0
  287. provekit/templates/flows/insurance-extract-to-json.yaml +43 -0
  288. provekit/templates/flows/insurance-faq-match-answer.yaml +60 -0
  289. provekit/templates/flows/insurance-keyword-extract.yaml +43 -0
  290. provekit/templates/flows/insurance-language-route.yaml +103 -0
  291. provekit/templates/flows/insurance-moderation-gate.yaml +73 -0
  292. provekit/templates/flows/insurance-redact-summarize.yaml +59 -0
  293. provekit/templates/flows/insurance-score-gate.yaml +73 -0
  294. provekit/templates/flows/insurance-sentiment-route.yaml +73 -0
  295. provekit/templates/flows/insurance-severity-tiers.yaml +103 -0
  296. provekit/templates/flows/insurance-summarize-then-decide.yaml +88 -0
  297. provekit/templates/flows/insurance-tag-enrich.yaml +43 -0
  298. provekit/templates/flows/insurance-tone-rewrite.yaml +43 -0
  299. provekit/templates/flows/insurance-translate-reply.yaml +58 -0
  300. provekit/templates/flows/insurance-triage-route.yaml +73 -0
  301. provekit/templates/flows/it-helpdesk-action-items.yaml +43 -0
  302. provekit/templates/flows/it-helpdesk-answer.yaml +43 -0
  303. provekit/templates/flows/it-helpdesk-classify-intent.yaml +43 -0
  304. provekit/templates/flows/it-helpdesk-draft-critique-revise.yaml +75 -0
  305. provekit/templates/flows/it-helpdesk-extract-to-json.yaml +43 -0
  306. provekit/templates/flows/it-helpdesk-faq-match-answer.yaml +60 -0
  307. provekit/templates/flows/it-helpdesk-keyword-extract.yaml +43 -0
  308. provekit/templates/flows/it-helpdesk-language-route.yaml +103 -0
  309. provekit/templates/flows/it-helpdesk-moderation-gate.yaml +73 -0
  310. provekit/templates/flows/it-helpdesk-redact-summarize.yaml +59 -0
  311. provekit/templates/flows/it-helpdesk-score-gate.yaml +73 -0
  312. provekit/templates/flows/it-helpdesk-sentiment-route.yaml +73 -0
  313. provekit/templates/flows/it-helpdesk-severity-tiers.yaml +103 -0
  314. provekit/templates/flows/it-helpdesk-summarize-then-decide.yaml +88 -0
  315. provekit/templates/flows/it-helpdesk-tag-enrich.yaml +43 -0
  316. provekit/templates/flows/it-helpdesk-tone-rewrite.yaml +43 -0
  317. provekit/templates/flows/it-helpdesk-translate-reply.yaml +58 -0
  318. provekit/templates/flows/it-helpdesk-triage-route.yaml +73 -0
  319. provekit/templates/flows/legal-intake-action-items.yaml +43 -0
  320. provekit/templates/flows/legal-intake-answer.yaml +43 -0
  321. provekit/templates/flows/legal-intake-classify-intent.yaml +43 -0
  322. provekit/templates/flows/legal-intake-draft-critique-revise.yaml +75 -0
  323. provekit/templates/flows/legal-intake-extract-to-json.yaml +43 -0
  324. provekit/templates/flows/legal-intake-faq-match-answer.yaml +60 -0
  325. provekit/templates/flows/legal-intake-keyword-extract.yaml +43 -0
  326. provekit/templates/flows/legal-intake-language-route.yaml +103 -0
  327. provekit/templates/flows/legal-intake-moderation-gate.yaml +73 -0
  328. provekit/templates/flows/legal-intake-redact-summarize.yaml +59 -0
  329. provekit/templates/flows/legal-intake-score-gate.yaml +73 -0
  330. provekit/templates/flows/legal-intake-sentiment-route.yaml +73 -0
  331. provekit/templates/flows/legal-intake-severity-tiers.yaml +103 -0
  332. provekit/templates/flows/legal-intake-summarize-then-decide.yaml +88 -0
  333. provekit/templates/flows/legal-intake-tag-enrich.yaml +43 -0
  334. provekit/templates/flows/legal-intake-tone-rewrite.yaml +43 -0
  335. provekit/templates/flows/legal-intake-translate-reply.yaml +58 -0
  336. provekit/templates/flows/legal-intake-triage-route.yaml +73 -0
  337. provekit/templates/flows/logistics-action-items.yaml +43 -0
  338. provekit/templates/flows/logistics-answer.yaml +43 -0
  339. provekit/templates/flows/logistics-classify-intent.yaml +43 -0
  340. provekit/templates/flows/logistics-draft-critique-revise.yaml +75 -0
  341. provekit/templates/flows/logistics-extract-to-json.yaml +43 -0
  342. provekit/templates/flows/logistics-faq-match-answer.yaml +60 -0
  343. provekit/templates/flows/logistics-keyword-extract.yaml +43 -0
  344. provekit/templates/flows/logistics-language-route.yaml +103 -0
  345. provekit/templates/flows/logistics-moderation-gate.yaml +73 -0
  346. provekit/templates/flows/logistics-redact-summarize.yaml +59 -0
  347. provekit/templates/flows/logistics-score-gate.yaml +73 -0
  348. provekit/templates/flows/logistics-sentiment-route.yaml +73 -0
  349. provekit/templates/flows/logistics-severity-tiers.yaml +103 -0
  350. provekit/templates/flows/logistics-summarize-then-decide.yaml +88 -0
  351. provekit/templates/flows/logistics-tag-enrich.yaml +43 -0
  352. provekit/templates/flows/logistics-tone-rewrite.yaml +43 -0
  353. provekit/templates/flows/logistics-translate-reply.yaml +58 -0
  354. provekit/templates/flows/logistics-triage-route.yaml +73 -0
  355. provekit/templates/flows/marketing-action-items.yaml +43 -0
  356. provekit/templates/flows/marketing-answer.yaml +43 -0
  357. provekit/templates/flows/marketing-classify-intent.yaml +43 -0
  358. provekit/templates/flows/marketing-draft-critique-revise.yaml +75 -0
  359. provekit/templates/flows/marketing-extract-to-json.yaml +43 -0
  360. provekit/templates/flows/marketing-faq-match-answer.yaml +60 -0
  361. provekit/templates/flows/marketing-keyword-extract.yaml +43 -0
  362. provekit/templates/flows/marketing-language-route.yaml +103 -0
  363. provekit/templates/flows/marketing-moderation-gate.yaml +73 -0
  364. provekit/templates/flows/marketing-redact-summarize.yaml +59 -0
  365. provekit/templates/flows/marketing-score-gate.yaml +73 -0
  366. provekit/templates/flows/marketing-sentiment-route.yaml +73 -0
  367. provekit/templates/flows/marketing-severity-tiers.yaml +103 -0
  368. provekit/templates/flows/marketing-summarize-then-decide.yaml +88 -0
  369. provekit/templates/flows/marketing-tag-enrich.yaml +43 -0
  370. provekit/templates/flows/marketing-tone-rewrite.yaml +43 -0
  371. provekit/templates/flows/marketing-translate-reply.yaml +58 -0
  372. provekit/templates/flows/marketing-triage-route.yaml +73 -0
  373. provekit/templates/flows/nonprofit-action-items.yaml +43 -0
  374. provekit/templates/flows/nonprofit-answer.yaml +43 -0
  375. provekit/templates/flows/nonprofit-classify-intent.yaml +43 -0
  376. provekit/templates/flows/nonprofit-draft-critique-revise.yaml +75 -0
  377. provekit/templates/flows/nonprofit-extract-to-json.yaml +43 -0
  378. provekit/templates/flows/nonprofit-faq-match-answer.yaml +60 -0
  379. provekit/templates/flows/nonprofit-keyword-extract.yaml +43 -0
  380. provekit/templates/flows/nonprofit-language-route.yaml +103 -0
  381. provekit/templates/flows/nonprofit-moderation-gate.yaml +73 -0
  382. provekit/templates/flows/nonprofit-redact-summarize.yaml +59 -0
  383. provekit/templates/flows/nonprofit-score-gate.yaml +73 -0
  384. provekit/templates/flows/nonprofit-sentiment-route.yaml +73 -0
  385. provekit/templates/flows/nonprofit-severity-tiers.yaml +103 -0
  386. provekit/templates/flows/nonprofit-summarize-then-decide.yaml +88 -0
  387. provekit/templates/flows/nonprofit-tag-enrich.yaml +43 -0
  388. provekit/templates/flows/nonprofit-tone-rewrite.yaml +43 -0
  389. provekit/templates/flows/nonprofit-translate-reply.yaml +58 -0
  390. provekit/templates/flows/nonprofit-triage-route.yaml +73 -0
  391. provekit/templates/flows/real-estate-action-items.yaml +43 -0
  392. provekit/templates/flows/real-estate-answer.yaml +43 -0
  393. provekit/templates/flows/real-estate-classify-intent.yaml +43 -0
  394. provekit/templates/flows/real-estate-draft-critique-revise.yaml +75 -0
  395. provekit/templates/flows/real-estate-extract-to-json.yaml +43 -0
  396. provekit/templates/flows/real-estate-faq-match-answer.yaml +60 -0
  397. provekit/templates/flows/real-estate-keyword-extract.yaml +43 -0
  398. provekit/templates/flows/real-estate-language-route.yaml +103 -0
  399. provekit/templates/flows/real-estate-moderation-gate.yaml +73 -0
  400. provekit/templates/flows/real-estate-redact-summarize.yaml +59 -0
  401. provekit/templates/flows/real-estate-score-gate.yaml +73 -0
  402. provekit/templates/flows/real-estate-sentiment-route.yaml +73 -0
  403. provekit/templates/flows/real-estate-severity-tiers.yaml +103 -0
  404. provekit/templates/flows/real-estate-summarize-then-decide.yaml +88 -0
  405. provekit/templates/flows/real-estate-tag-enrich.yaml +43 -0
  406. provekit/templates/flows/real-estate-tone-rewrite.yaml +43 -0
  407. provekit/templates/flows/real-estate-translate-reply.yaml +58 -0
  408. provekit/templates/flows/real-estate-triage-route.yaml +73 -0
  409. provekit/templates/flows/recruiting-action-items.yaml +43 -0
  410. provekit/templates/flows/recruiting-answer.yaml +43 -0
  411. provekit/templates/flows/recruiting-classify-intent.yaml +43 -0
  412. provekit/templates/flows/recruiting-draft-critique-revise.yaml +75 -0
  413. provekit/templates/flows/recruiting-extract-to-json.yaml +43 -0
  414. provekit/templates/flows/recruiting-faq-match-answer.yaml +60 -0
  415. provekit/templates/flows/recruiting-keyword-extract.yaml +43 -0
  416. provekit/templates/flows/recruiting-language-route.yaml +103 -0
  417. provekit/templates/flows/recruiting-moderation-gate.yaml +73 -0
  418. provekit/templates/flows/recruiting-redact-summarize.yaml +59 -0
  419. provekit/templates/flows/recruiting-score-gate.yaml +73 -0
  420. provekit/templates/flows/recruiting-sentiment-route.yaml +73 -0
  421. provekit/templates/flows/recruiting-severity-tiers.yaml +103 -0
  422. provekit/templates/flows/recruiting-summarize-then-decide.yaml +88 -0
  423. provekit/templates/flows/recruiting-tag-enrich.yaml +43 -0
  424. provekit/templates/flows/recruiting-tone-rewrite.yaml +43 -0
  425. provekit/templates/flows/recruiting-translate-reply.yaml +58 -0
  426. provekit/templates/flows/recruiting-triage-route.yaml +73 -0
  427. provekit/templates/flows/research-action-items.yaml +43 -0
  428. provekit/templates/flows/research-answer.yaml +43 -0
  429. provekit/templates/flows/research-classify-intent.yaml +43 -0
  430. provekit/templates/flows/research-draft-critique-revise.yaml +75 -0
  431. provekit/templates/flows/research-extract-to-json.yaml +43 -0
  432. provekit/templates/flows/research-faq-match-answer.yaml +60 -0
  433. provekit/templates/flows/research-keyword-extract.yaml +43 -0
  434. provekit/templates/flows/research-language-route.yaml +103 -0
  435. provekit/templates/flows/research-moderation-gate.yaml +73 -0
  436. provekit/templates/flows/research-redact-summarize.yaml +59 -0
  437. provekit/templates/flows/research-score-gate.yaml +73 -0
  438. provekit/templates/flows/research-sentiment-route.yaml +73 -0
  439. provekit/templates/flows/research-severity-tiers.yaml +103 -0
  440. provekit/templates/flows/research-summarize-then-decide.yaml +88 -0
  441. provekit/templates/flows/research-tag-enrich.yaml +43 -0
  442. provekit/templates/flows/research-tone-rewrite.yaml +43 -0
  443. provekit/templates/flows/research-translate-reply.yaml +58 -0
  444. provekit/templates/flows/research-triage-route.yaml +73 -0
  445. provekit/templates/flows/sales-lead-action-items.yaml +43 -0
  446. provekit/templates/flows/sales-lead-answer.yaml +43 -0
  447. provekit/templates/flows/sales-lead-classify-intent.yaml +43 -0
  448. provekit/templates/flows/sales-lead-draft-critique-revise.yaml +75 -0
  449. provekit/templates/flows/sales-lead-extract-to-json.yaml +43 -0
  450. provekit/templates/flows/sales-lead-faq-match-answer.yaml +60 -0
  451. provekit/templates/flows/sales-lead-keyword-extract.yaml +43 -0
  452. provekit/templates/flows/sales-lead-language-route.yaml +103 -0
  453. provekit/templates/flows/sales-lead-moderation-gate.yaml +73 -0
  454. provekit/templates/flows/sales-lead-redact-summarize.yaml +59 -0
  455. provekit/templates/flows/sales-lead-score-gate.yaml +73 -0
  456. provekit/templates/flows/sales-lead-sentiment-route.yaml +73 -0
  457. provekit/templates/flows/sales-lead-severity-tiers.yaml +103 -0
  458. provekit/templates/flows/sales-lead-summarize-then-decide.yaml +88 -0
  459. provekit/templates/flows/sales-lead-tag-enrich.yaml +43 -0
  460. provekit/templates/flows/sales-lead-tone-rewrite.yaml +43 -0
  461. provekit/templates/flows/sales-lead-translate-reply.yaml +58 -0
  462. provekit/templates/flows/sales-lead-triage-route.yaml +73 -0
  463. provekit/templates/flows/travel-action-items.yaml +43 -0
  464. provekit/templates/flows/travel-answer.yaml +43 -0
  465. provekit/templates/flows/travel-classify-intent.yaml +43 -0
  466. provekit/templates/flows/travel-draft-critique-revise.yaml +75 -0
  467. provekit/templates/flows/travel-extract-to-json.yaml +43 -0
  468. provekit/templates/flows/travel-faq-match-answer.yaml +60 -0
  469. provekit/templates/flows/travel-keyword-extract.yaml +43 -0
  470. provekit/templates/flows/travel-language-route.yaml +103 -0
  471. provekit/templates/flows/travel-moderation-gate.yaml +73 -0
  472. provekit/templates/flows/travel-redact-summarize.yaml +59 -0
  473. provekit/templates/flows/travel-score-gate.yaml +73 -0
  474. provekit/templates/flows/travel-sentiment-route.yaml +73 -0
  475. provekit/templates/flows/travel-severity-tiers.yaml +103 -0
  476. provekit/templates/flows/travel-summarize-then-decide.yaml +88 -0
  477. provekit/templates/flows/travel-tag-enrich.yaml +43 -0
  478. provekit/templates/flows/travel-tone-rewrite.yaml +43 -0
  479. provekit/templates/flows/travel-translate-reply.yaml +58 -0
  480. provekit/templates/flows/travel-triage-route.yaml +73 -0
  481. provekit-0.1.0.dist-info/METADATA +86 -0
  482. provekit-0.1.0.dist-info/RECORD +485 -0
  483. provekit-0.1.0.dist-info/WHEEL +5 -0
  484. provekit-0.1.0.dist-info/entry_points.txt +2 -0
  485. provekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,150 @@
1
+ """Auth endpoints: register / login / logout / me (email + password).
2
+
3
+ Sessions are httpOnly cookies carrying a signed token. GitHub OAuth is a planned
4
+ provider; the User model + session layer are provider-agnostic so it slots in later.
5
+ """
6
+ from fastapi import APIRouter, Depends, HTTPException, Request, Response
7
+ from pydantic import BaseModel, EmailStr
8
+ from sqlalchemy.orm import Session
9
+
10
+ from ..config import get_settings
11
+ from ..database import get_db
12
+ from ..models import User
13
+ from ..services import auth, email
14
+ from ..services.limits import check_login_rate
15
+
16
+ router = APIRouter(prefix="/api/auth", tags=["auth"])
17
+
18
+
19
+ class Credentials(BaseModel):
20
+ email: EmailStr
21
+ password: str
22
+ name: str = ""
23
+
24
+
25
+ def _set_cookie(resp: Response, u: User) -> None:
26
+ token = auth.make_token(u.id, ver=u.token_version)
27
+ resp.set_cookie(auth.COOKIE, token, max_age=auth._TTL, httponly=True,
28
+ samesite="lax", secure=get_settings().hosted, path="/")
29
+
30
+
31
+ def _public(u: User) -> dict:
32
+ return {"id": u.id, "email": u.email, "name": u.name, "auth_provider": u.auth_provider,
33
+ "email_verified": u.email_verified}
34
+
35
+
36
+ def _send_verify(u: User) -> None:
37
+ s = get_settings()
38
+ token = auth.make_token(u.id, ttl=2 * 24 * 3600, purpose="verify", ver=u.token_version)
39
+ link = f"{s.web_base_url.rstrip('/')}/verify?token={token}"
40
+ email.send(u.email, "Verify your ProveKit email",
41
+ f"Confirm your email to finish setting up ProveKit:\n\n{link}\n\nThis link expires in 48 hours.")
42
+
43
+
44
+ @router.post("/register")
45
+ def register(body: Credentials, response: Response, db: Session = Depends(get_db)):
46
+ if db.query(User).filter(User.email == body.email).first():
47
+ raise HTTPException(409, "An account with that email already exists")
48
+ if len(body.password) < 8:
49
+ raise HTTPException(400, "Password must be at least 8 characters")
50
+ u = User(email=body.email, name=body.name or body.email.split("@")[0],
51
+ password_hash=auth.hash_password(body.password))
52
+ db.add(u); db.commit(); db.refresh(u)
53
+ _send_verify(u)
54
+ if not get_settings().require_email_verification:
55
+ _set_cookie(response, u) # verification optional → log in immediately
56
+ return _public(u)
57
+
58
+
59
+ @router.post("/login")
60
+ def login(body: Credentials, request: Request, response: Response, db: Session = Depends(get_db)):
61
+ client = request.client.host if request.client else "?"
62
+ check_login_rate(f"{body.email}:{client}") # brute-force throttle
63
+ u = db.query(User).filter(User.email == body.email).first()
64
+ if not u or not u.password_hash:
65
+ # Spend the same PBKDF2 cost on a missing/OAuth account so response time doesn't
66
+ # reveal whether the email exists (timing-based account enumeration).
67
+ auth.verify_password(body.password, auth.DUMMY_HASH)
68
+ raise HTTPException(401, "Invalid email or password")
69
+ if not auth.verify_password(body.password, u.password_hash):
70
+ raise HTTPException(401, "Invalid email or password")
71
+ if get_settings().require_email_verification and not u.email_verified:
72
+ raise HTTPException(403, "Please verify your email before signing in.")
73
+ _set_cookie(response, u)
74
+ return _public(u)
75
+
76
+
77
+ @router.post("/logout")
78
+ def logout(response: Response):
79
+ response.delete_cookie(auth.COOKIE, path="/")
80
+ return {"ok": True}
81
+
82
+
83
+ @router.get("/me")
84
+ def me(user: User = Depends(auth.get_current_user)):
85
+ return _public(user)
86
+
87
+
88
+ class Email(BaseModel):
89
+ email: EmailStr
90
+
91
+
92
+ @router.post("/forgot")
93
+ def forgot(body: Email, request: Request, db: Session = Depends(get_db)):
94
+ """Email a password-reset link. Always returns ok — never leaks whether the email exists."""
95
+ client = request.client.host if request.client else "?"
96
+ check_login_rate(f"forgot:{body.email}:{client}")
97
+ u = db.query(User).filter(User.email == body.email).first()
98
+ if u and u.password_hash:
99
+ token = auth.make_token(u.id, ttl=3600, purpose="reset", ver=u.token_version)
100
+ link = f"{get_settings().web_base_url.rstrip('/')}/reset?token={token}"
101
+ email.send(u.email, "Reset your ProveKit password",
102
+ f"Reset your password with this link (valid 1 hour):\n\n{link}\n\n"
103
+ "If you didn't request this, ignore this email.")
104
+ return {"ok": True}
105
+
106
+
107
+ class ResetIn(BaseModel):
108
+ token: str
109
+ password: str
110
+
111
+
112
+ @router.post("/reset")
113
+ def reset(body: ResetIn, db: Session = Depends(get_db)):
114
+ claims = auth.read_token(body.token, purpose="reset")
115
+ if claims is None:
116
+ raise HTTPException(400, "This reset link is invalid or has expired.")
117
+ if len(body.password) < 8:
118
+ raise HTTPException(400, "Password must be at least 8 characters")
119
+ uid, ver = claims
120
+ u = db.get(User, uid)
121
+ if not u or u.token_version != ver: # a reset link is single-use and dies on the next reset
122
+ raise HTTPException(400, "This reset link is invalid or has expired.")
123
+ u.password_hash = auth.hash_password(body.password)
124
+ # Receiving this link proves control of the mailbox — the same proof the verify link
125
+ # provides — so consume it as verification too. Without this, bumping token_version
126
+ # below would kill the registration verify link (the only one ever minted) and, with
127
+ # REQUIRE_EMAIL_VERIFICATION, lock the account out permanently.
128
+ u.email_verified = True
129
+ u.token_version += 1 # revoke every existing session + this now-used reset link
130
+ db.commit()
131
+ return {"ok": True}
132
+
133
+
134
+ class TokenIn(BaseModel):
135
+ token: str
136
+
137
+
138
+ @router.post("/verify")
139
+ def verify(body: TokenIn, response: Response, db: Session = Depends(get_db)):
140
+ claims = auth.read_token(body.token, purpose="verify")
141
+ if claims is None:
142
+ raise HTTPException(400, "This verification link is invalid or has expired.")
143
+ uid, ver = claims
144
+ u = db.get(User, uid)
145
+ if not u or u.token_version != ver:
146
+ raise HTTPException(400, "This verification link is invalid or has expired.")
147
+ u.email_verified = True
148
+ db.commit()
149
+ _set_cookie(response, u) # verifying signs you in
150
+ return _public(u)
@@ -0,0 +1,270 @@
1
+ """Connections (providers): llm | mcp | agent. Secrets are masked in responses and
2
+ preserved on update when the client sends a masked/empty key."""
3
+ import httpx
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from pydantic import BaseModel
6
+ from sqlalchemy.orm import Session
7
+
8
+ from ..database import get_db
9
+ from ..models import Connection, Workspace, iso_utc
10
+ from ..services.assertions import get_path
11
+ from ..services.masking import is_masked, mask_headers, mask_value
12
+ from ..services.netguard import BlockedURL, guard_url
13
+ from ..services.providers.mcp_client import MCPSession
14
+ from ..services.workspace import current_workspace
15
+
16
+
17
+ def _guard_url(url: str) -> None:
18
+ """Shared SSRF guard (services.netguard), surfaced as a 400 for router callers."""
19
+ try:
20
+ guard_url(url)
21
+ except BlockedURL as exc:
22
+ raise HTTPException(400, str(exc))
23
+
24
+
25
+ def _get(db: Session, ws: Workspace, cid: int) -> Connection:
26
+ """Fetch a connection scoped to the workspace (404 across workspaces)."""
27
+ c = db.get(Connection, cid)
28
+ if not c or c.workspace_id != ws.id:
29
+ raise HTTPException(404, "Connection not found")
30
+ return c
31
+
32
+
33
+ router = APIRouter(prefix="/api/connections", tags=["connections"])
34
+
35
+
36
+ def _public(c: Connection) -> dict:
37
+ cfg = dict(c.config or {})
38
+ if cfg.get("api_key"):
39
+ cfg["api_key"] = mask_value(cfg["api_key"])
40
+ cfg["has_key"] = True
41
+ hdrs = cfg.get("headers")
42
+ if isinstance(hdrs, dict) and hdrs:
43
+ cfg["headers"] = mask_headers(hdrs)
44
+ oauth = cfg.get("oauth")
45
+ if isinstance(oauth, dict) and oauth.get("client_secret"):
46
+ cfg["oauth"] = {**oauth, "client_secret": mask_value(oauth["client_secret"])}
47
+ env = cfg.get("env")
48
+ if isinstance(env, dict) and env:
49
+ cfg["env"] = {k: mask_value(v) for k, v in env.items()}
50
+ return {"id": c.id, "name": c.name, "kind": c.kind, "config": cfg,
51
+ "created_at": iso_utc(c.created_at)}
52
+
53
+
54
+ class ConnectionIn(BaseModel):
55
+ name: str
56
+ kind: str # llm | mcp | agent
57
+ config: dict = {}
58
+
59
+
60
+ def _restore_secrets(incoming: dict, stored: dict) -> dict:
61
+ """Map each masked value back to the stored secret it stands for, by key.
62
+
63
+ A masked value is a display placeholder, never a credential. If the key has no stored
64
+ counterpart — the user renamed an env var or header without retyping its value — the
65
+ entry is dropped rather than persisted, which would otherwise seal the literal
66
+ "••••3456" as the credential and fail later with no clue why.
67
+ """
68
+ out = {}
69
+ for k, v in incoming.items():
70
+ if not is_masked(v):
71
+ out[k] = v
72
+ elif k in stored:
73
+ out[k] = stored[k]
74
+ return out
75
+
76
+
77
+ @router.get("")
78
+ def list_connections(db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
79
+ return [_public(c) for c in db.query(Connection).filter(Connection.workspace_id == ws.id).order_by(Connection.id).all()]
80
+
81
+
82
+ @router.post("")
83
+ def create_connection(payload: ConnectionIn, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
84
+ # A new connection has nothing stored to restore a mask from, so any masked value here
85
+ # (a UI "duplicate" that round-trips a GET response) is dropped, not saved as a secret.
86
+ cfg = dict(payload.config or {})
87
+ if is_masked(cfg.get("api_key")):
88
+ cfg["api_key"] = ""
89
+ if isinstance(cfg.get("oauth"), dict) and is_masked(cfg["oauth"].get("client_secret")):
90
+ cfg["oauth"] = {**cfg["oauth"], "client_secret": ""}
91
+ for field in ("headers", "env"):
92
+ if isinstance(cfg.get(field), dict):
93
+ cfg[field] = _restore_secrets(cfg[field], {})
94
+ cfg.pop("has_key", None)
95
+ c = Connection(workspace_id=ws.id, name=payload.name, kind=payload.kind, config=cfg)
96
+ db.add(c); db.commit(); db.refresh(c)
97
+ return _public(c)
98
+
99
+
100
+ @router.put("/{cid}")
101
+ def update_connection(cid: int, payload: ConnectionIn, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
102
+ c = _get(db, ws, cid)
103
+ cfg = dict(payload.config or {})
104
+ stored = c.config or {}
105
+ # Preserve the stored key if the client didn't send a fresh one.
106
+ incoming = cfg.get("api_key", "")
107
+ if not incoming or is_masked(incoming):
108
+ cfg["api_key"] = stored.get("api_key", "")
109
+ # Same for masked secret headers — don't let a masked value overwrite the real token.
110
+ if isinstance(cfg.get("headers"), dict):
111
+ cfg["headers"] = _restore_secrets(cfg["headers"], stored.get("headers") or {})
112
+ # Preserve a masked/blank MCP OAuth client_secret rather than clobbering the stored one.
113
+ oauth = cfg.get("oauth")
114
+ if isinstance(oauth, dict):
115
+ cs = oauth.get("client_secret", "")
116
+ if isinstance(cs, str) and (not cs or is_masked(cs)):
117
+ cfg["oauth"] = {**oauth, "client_secret": (stored.get("oauth") or {}).get("client_secret", "")}
118
+ # Same for masked stdio env values.
119
+ if isinstance(cfg.get("env"), dict):
120
+ cfg["env"] = _restore_secrets(cfg["env"], stored.get("env") or {})
121
+ cfg.pop("has_key", None)
122
+ c.name, c.kind, c.config = payload.name, payload.kind, cfg
123
+ db.commit(); db.refresh(c)
124
+ return _public(c)
125
+
126
+
127
+ @router.delete("/{cid}")
128
+ def delete_connection(cid: int, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
129
+ c = db.get(Connection, cid)
130
+ if c and c.workspace_id == ws.id:
131
+ db.delete(c); db.commit()
132
+ return {"deleted": True}
133
+
134
+
135
+ class AuthPayload(BaseModel):
136
+ login_path: str = "/api/auth/login"
137
+ method: str = "POST"
138
+ body: dict = {}
139
+ token_path: str = "token"
140
+ header: str = "Authorization"
141
+ scheme: str = "Bearer"
142
+
143
+
144
+ @router.post("/{cid}/authenticate")
145
+ def authenticate(cid: int, payload: AuthPayload, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
146
+ """Perform a login against the agent's base URL, extract the token, and store it as a
147
+ default header on the connection. Credentials are used transiently — only the token
148
+ (which expires) is saved, never the username/password."""
149
+ c = _get(db, ws, cid)
150
+ if c.kind != "agent":
151
+ raise HTTPException(400, "Not an agent connection")
152
+ base = (c.config or {}).get("base_url")
153
+ if not base:
154
+ raise HTTPException(400, "Connection has no base_url")
155
+ url = base.rstrip("/") + "/" + payload.login_path.lstrip("/")
156
+ _guard_url(url)
157
+ try:
158
+ r = httpx.request(payload.method.upper(), url, json=payload.body or None, timeout=30)
159
+ r.raise_for_status()
160
+ data = r.json()
161
+ except Exception as exc:
162
+ raise HTTPException(502, f"Login failed: {exc}")
163
+ token = get_path(data, payload.token_path)
164
+ if not token:
165
+ raise HTTPException(400, f"No token found at '{payload.token_path}' in the login response")
166
+ headers = dict((c.config or {}).get("headers") or {})
167
+ headers[payload.header] = f"{payload.scheme} {token}".strip()
168
+ c.config = {**(c.config or {}), "headers": headers}
169
+ db.commit()
170
+ tok = str(token)
171
+ return {"ok": True, "header": payload.header, "token": (tok[:6] + "…" + tok[-4:]) if len(tok) > 12 else "••••"}
172
+
173
+
174
+ DEFAULT_BASE = {"openai": "https://api.openai.com/v1", "openai-responses": "https://api.openai.com/v1",
175
+ "anthropic": "https://api.anthropic.com/v1"}
176
+
177
+
178
+ @router.post("/{cid}/test")
179
+ def test_connection(cid: int, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
180
+ """Live reachability/auth check so onboarding can confirm a connection works before use.
181
+ llm → list models · mcp → list tools · agent → GET the base URL."""
182
+ c = _get(db, ws, cid)
183
+ cfg = c.config or {}
184
+ try:
185
+ if c.kind == "mcp":
186
+ if not cfg.get("url") and not cfg.get("command"):
187
+ return {"ok": False, "detail": "No server URL or command set"}
188
+ tools = _mcp_from_cfg(cfg).list_tools()
189
+ return {"ok": True, "detail": f"{len(tools)} tool{'s' if len(tools) != 1 else ''} discovered"}
190
+ if c.kind == "llm":
191
+ provider = cfg.get("provider", "openai")
192
+ if provider == "mock":
193
+ return {"ok": True, "detail": "Demo agent ready — no key needed"}
194
+ base = (cfg.get("base_url") or DEFAULT_BASE.get(provider, "")).rstrip("/")
195
+ key = cfg.get("api_key") or ""
196
+ if not key:
197
+ return {"ok": False, "detail": "No API key set"}
198
+ if not base:
199
+ return {"ok": False, "detail": "No base URL set"}
200
+ _guard_url(base)
201
+ if provider == "anthropic":
202
+ headers = {"x-api-key": key, "anthropic-version": "2023-06-01"}
203
+ else:
204
+ headers = {"Authorization": f"Bearer {key}"}
205
+ r = httpx.get(base + "/models", headers=headers, timeout=15)
206
+ if r.status_code == 200:
207
+ n = len(((r.json() or {}).get("data")) or [])
208
+ return {"ok": True, "detail": f"Authenticated — {n} models available" if n else "Authenticated ✓"}
209
+ if r.status_code in (401, 403):
210
+ return {"ok": False, "detail": "Key rejected (401/403) — check the API key"}
211
+ return {"ok": False, "detail": f"Provider returned {r.status_code}"}
212
+ if c.kind == "a2a":
213
+ base = (cfg.get("base_url") or "").rstrip("/")
214
+ if not base:
215
+ return {"ok": False, "detail": "No base URL set"}
216
+ _guard_url(base)
217
+ from ..services.providers.a2a_client import fetch_card
218
+ card = fetch_card(base, headers=cfg.get("headers"))
219
+ return {"ok": True, "detail": f"Agent card: {card.get('name')} (A2A {card.get('_version')})"}
220
+ # agent
221
+ base = (cfg.get("base_url") or "").rstrip("/")
222
+ if not base:
223
+ return {"ok": False, "detail": "No base URL set"}
224
+ _guard_url(base)
225
+ r = httpx.get(base, headers=cfg.get("headers") or None, timeout=15)
226
+ return {"ok": r.status_code < 500, "detail": f"Reachable — HTTP {r.status_code}"}
227
+ except Exception as exc:
228
+ return {"ok": False, "detail": f"Unreachable: {str(exc)[:120]}"}
229
+
230
+
231
+ def _mcp_from_cfg(cfg: dict) -> MCPSession:
232
+ """Build an MCP session (stdio | http, spec, oauth) from a connection config."""
233
+ if cfg.get("command"):
234
+ return MCPSession(command=cfg["command"], args=cfg.get("args"), env=cfg.get("env"),
235
+ spec=cfg.get("spec", "auto"))
236
+ _guard_url(cfg.get("url") or "")
237
+ return MCPSession(cfg.get("url"), headers=cfg.get("headers"),
238
+ spec=cfg.get("spec", "auto"), oauth=cfg.get("oauth"))
239
+
240
+
241
+ @router.get("/{cid}/agent-card")
242
+ def agent_card(cid: int, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
243
+ """Fetch + validate an A2A connection's agent card (for the request form / discovery)."""
244
+ c = _get(db, ws, cid)
245
+ if c.kind != "a2a":
246
+ raise HTTPException(400, "Not an A2A connection")
247
+ base = (c.config or {}).get("base_url")
248
+ if not base:
249
+ raise HTTPException(400, "A2A connection has no base_url")
250
+ _guard_url(base)
251
+ try:
252
+ from ..services.providers.a2a_client import fetch_card
253
+ return {"card": fetch_card(base, headers=(c.config or {}).get("headers"))}
254
+ except Exception as exc:
255
+ raise HTTPException(502, f"A2A error: {exc}")
256
+
257
+
258
+ @router.get("/{cid}/tools")
259
+ def list_tools(cid: int, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
260
+ """Discover the tools exposed by an MCP connection (for the tool request form)."""
261
+ c = _get(db, ws, cid)
262
+ if c.kind != "mcp":
263
+ raise HTTPException(400, "Not an MCP connection")
264
+ cfg = c.config or {}
265
+ if not cfg.get("url") and not cfg.get("command"):
266
+ raise HTTPException(400, "MCP connection has no url or command")
267
+ try:
268
+ return {"tools": _mcp_from_cfg(cfg).list_tools()}
269
+ except Exception as exc:
270
+ raise HTTPException(502, f"MCP error: {exc}")
@@ -0,0 +1,161 @@
1
+ """Deployments — publish a flow as a hosted, versioned endpoint. Workspace-scoped.
2
+ The public invocation route lives in routers/runtime.py."""
3
+ from fastapi import APIRouter, Depends, HTTPException
4
+ from pydantic import BaseModel
5
+ from sqlalchemy.orm import Session
6
+
7
+ from ..config import get_settings
8
+ from ..database import get_db
9
+ from ..models import Deployment, Flow, Run, Workspace, iso_utc
10
+ from ..services import deploy
11
+ from ..services.workspace import current_workspace
12
+
13
+ router = APIRouter(prefix="/api/deployments", tags=["deployments"])
14
+
15
+
16
+ def _public(d: Deployment, url: str | None = None) -> dict:
17
+ out = {"id": d.id, "slug": d.slug, "version": d.version, "name": d.name,
18
+ "flow_id": d.flow_id, "active": d.active, "created_at": iso_utc(d.created_at)}
19
+ if url:
20
+ out["url"] = url
21
+ return out
22
+
23
+
24
+ def _endpoint(slug: str) -> str:
25
+ base = get_settings().public_base_url.rstrip("/")
26
+ return f"{base}/v1/d/{slug}"
27
+
28
+
29
+ class DeployIn(BaseModel):
30
+ flow_id: int
31
+
32
+
33
+ @router.post("")
34
+ def create_deployment(payload: DeployIn, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
35
+ """Publish a flow. First publish mints an API key (returned once); redeploys of the
36
+ same flow bump the version, snapshot the current graph, and keep the key."""
37
+ f = db.get(Flow, payload.flow_id)
38
+ if not f or f.workspace_id != ws.id:
39
+ raise HTTPException(404, "Flow not found")
40
+
41
+ prior = (db.query(Deployment)
42
+ .filter(Deployment.workspace_id == ws.id, Deployment.flow_id == f.id)
43
+ .order_by(Deployment.version.desc()).first())
44
+ snapshot = {"nodes": f.nodes, "edges": f.edges}
45
+ plaintext = None
46
+ if prior:
47
+ key_hash, slug, version = prior.api_key_hash, prior.slug, prior.version + 1
48
+ for d in db.query(Deployment).filter(Deployment.slug == slug, Deployment.active.is_(True)).all():
49
+ d.active = False
50
+ else:
51
+ plaintext, key_hash = deploy.new_api_key()
52
+ slug, version = _unique_slug(db, deploy.slugify(f.name)), 1
53
+
54
+ d = Deployment(workspace_id=ws.id, flow_id=f.id, slug=slug, version=version,
55
+ name=f.name, snapshot=snapshot, api_key_hash=key_hash, active=True)
56
+ db.add(d); db.commit(); db.refresh(d)
57
+ resp = _public(d, _endpoint(slug))
58
+ if plaintext: # shown exactly once
59
+ resp["api_key"] = plaintext
60
+ return resp
61
+
62
+
63
+ def _unique_slug(db, base: str) -> str:
64
+ slug, n = base, 2
65
+ while db.query(Deployment).filter(Deployment.slug == slug).first():
66
+ slug = f"{base}-{n}"; n += 1
67
+ return slug
68
+
69
+
70
+ @router.get("")
71
+ def list_deployments(db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
72
+ """One row per slug: the version actually being served, plus the version count.
73
+
74
+ Picks the *active* row, falling back to the latest when nothing is active. Always
75
+ picking the latest would misreport a rolled-back slug as inactive while an older
76
+ version keeps answering /v1/d/{slug} — see runtime.serve().
77
+ """
78
+ rows = db.query(Deployment).filter(Deployment.workspace_id == ws.id).order_by(Deployment.id.desc()).all()
79
+ serving: dict[str, Deployment] = {}
80
+ counts: dict[str, int] = {}
81
+ latest: dict[str, int] = {}
82
+ for d in rows:
83
+ counts[d.slug] = counts.get(d.slug, 0) + 1
84
+ latest[d.slug] = max(latest.get(d.slug, 0), d.version)
85
+ cur = serving.get(d.slug)
86
+ if cur is None or (d.active, d.version) > (cur.active, cur.version):
87
+ serving[d.slug] = d
88
+ return [{**_public(d, _endpoint(slug)), "versions": counts[slug], "latest_version": latest[slug]}
89
+ for slug, d in serving.items()]
90
+
91
+
92
+ @router.get("/{slug}")
93
+ def get_deployment(slug: str, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
94
+ rows = (db.query(Deployment)
95
+ .filter(Deployment.workspace_id == ws.id, Deployment.slug == slug)
96
+ .order_by(Deployment.version.desc()).all())
97
+ if not rows:
98
+ raise HTTPException(404, "Deployment not found")
99
+ active = next((r for r in rows if r.active), rows[0])
100
+ return {**_public(active, _endpoint(slug)),
101
+ "versions": [{"version": r.version, "active": r.active, "created_at": iso_utc(r.created_at)} for r in rows]}
102
+
103
+
104
+ @router.get("/{slug}/runs")
105
+ def deployment_runs(slug: str, limit: int = 50, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
106
+ """Recent invocations of this deployment (all versions)."""
107
+ dep_ids = [d.id for d in db.query(Deployment.id).filter(Deployment.workspace_id == ws.id, Deployment.slug == slug).all()]
108
+ if not dep_ids:
109
+ raise HTTPException(404, "Deployment not found")
110
+ limit = max(1, min(limit, 200)) # clamp both ends (SQLite treats LIMIT -1 as unlimited)
111
+ rows = (db.query(Run).filter(Run.deployment_id.in_(dep_ids))
112
+ .order_by(Run.id.desc()).limit(limit).all())
113
+ return [{"id": r.id, "status": r.status, "duration_ms": r.duration_ms,
114
+ "created_at": iso_utc(r.created_at)} for r in rows]
115
+
116
+
117
+ @router.get("/{slug}/stats")
118
+ def deployment_stats(slug: str, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
119
+ """Aggregate metrics: invocations, error rate, p50/p95 latency."""
120
+ dep_ids = [d.id for d in db.query(Deployment.id).filter(Deployment.workspace_id == ws.id, Deployment.slug == slug).all()]
121
+ if not dep_ids:
122
+ raise HTTPException(404, "Deployment not found")
123
+ rows = db.query(Run.status, Run.duration_ms).filter(Run.deployment_id.in_(dep_ids)).all()
124
+ total = len(rows)
125
+ failed = sum(1 for s, _ in rows if s == "failed")
126
+ durs = sorted(d for _, d in rows if d is not None)
127
+
128
+ def pct(p):
129
+ if not durs:
130
+ return 0
131
+ return durs[min(len(durs) - 1, int(len(durs) * p))]
132
+ return {"invocations": total, "failed": failed,
133
+ "error_rate": round(failed / total, 4) if total else 0.0,
134
+ "p50_ms": pct(0.50), "p95_ms": pct(0.95)}
135
+
136
+
137
+ @router.post("/{slug}/deactivate")
138
+ def deactivate(slug: str, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
139
+ rows = db.query(Deployment).filter(Deployment.workspace_id == ws.id, Deployment.slug == slug).all()
140
+ if not rows:
141
+ raise HTTPException(404, "Deployment not found")
142
+ for r in rows:
143
+ r.active = False
144
+ db.commit()
145
+ return {"ok": True}
146
+
147
+
148
+ class RollbackIn(BaseModel):
149
+ version: int
150
+
151
+
152
+ @router.post("/{slug}/rollback")
153
+ def rollback(slug: str, payload: RollbackIn, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
154
+ rows = db.query(Deployment).filter(Deployment.workspace_id == ws.id, Deployment.slug == slug).all()
155
+ target = next((r for r in rows if r.version == payload.version), None)
156
+ if not target:
157
+ raise HTTPException(404, "Version not found")
158
+ for r in rows:
159
+ r.active = (r.version == payload.version)
160
+ db.commit()
161
+ return _public(target, _endpoint(slug))