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,289 @@
1
+ """Generic MCP client — the best test client for Model Context Protocol servers.
2
+
3
+ Transports: Streamable HTTP · stdio (spawn a local server process)
4
+ Spec generations: stateful 2025-11-25 (initialize handshake + Mcp-Session-Id)
5
+ stateless 2026-07-28 (no session, server/discover) · auto-detect
6
+ Auth: static bearer (via headers) · OAuth 2.1 client-credentials grant;
7
+ a 401 surfaces the resource-metadata hint so the user knows what's needed.
8
+ Discovery: tools · resources · prompts, all cursor-paginated.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import json
14
+ import subprocess
15
+ import threading
16
+
17
+ import httpx
18
+
19
+ from ..netguard import guard_stdio, guard_url
20
+
21
+ _ACCEPT = "application/json, text/event-stream"
22
+ _STATEFUL = "2025-11-25"
23
+ _STATELESS = "2026-07-28"
24
+ _lock = threading.Lock()
25
+ _id = 0
26
+
27
+
28
+ def _next_id() -> int:
29
+ global _id
30
+ with _lock:
31
+ _id += 1
32
+ return _id
33
+
34
+
35
+ class MCPError(RuntimeError):
36
+ pass
37
+
38
+
39
+ def _fetch_oauth_token(oauth: dict) -> str:
40
+ """Exchange client credentials for a bearer token (OAuth 2.1 client_credentials).
41
+ Interactive auth-code/PKCE flows need a browser and are out of scope here."""
42
+ token_url = oauth.get("token_url")
43
+ if not token_url:
44
+ raise MCPError("oauth config needs a token_url")
45
+ guard_url(token_url)
46
+ data = {"grant_type": "client_credentials",
47
+ "client_id": oauth.get("client_id", ""),
48
+ "client_secret": oauth.get("client_secret", "")}
49
+ if oauth.get("scope"):
50
+ data["scope"] = oauth["scope"]
51
+ if oauth.get("resource"): # RFC 8707 resource indicator
52
+ data["resource"] = oauth["resource"]
53
+ r = httpx.post(token_url, data=data, timeout=30, follow_redirects=False)
54
+ r.raise_for_status()
55
+ tok = r.json().get("access_token")
56
+ if not tok:
57
+ raise MCPError("no access_token in the OAuth token response")
58
+ return tok
59
+
60
+
61
+ class _HTTPTransport:
62
+ def __init__(self, url, headers, timeout, stateful: bool):
63
+ guard_url(url)
64
+ self.url = url.rstrip("/")
65
+ self.headers = {"Content-Type": "application/json", "Accept": _ACCEPT, **(headers or {})}
66
+ self.stateful = stateful
67
+ self.session_id: str | None = None
68
+ self._client = httpx.Client(timeout=timeout, follow_redirects=False)
69
+
70
+ def _hdrs(self):
71
+ h = dict(self.headers)
72
+ if self.session_id:
73
+ h["Mcp-Session-Id"] = self.session_id
74
+ return h
75
+
76
+ def request(self, payload) -> dict:
77
+ resp = self._client.post(self.url, json=payload, headers=self._hdrs())
78
+ sid = resp.headers.get("mcp-session-id")
79
+ if sid:
80
+ self.session_id = sid
81
+ if resp.status_code == 401:
82
+ meta = resp.headers.get("www-authenticate", "")
83
+ raise MCPError(f"401 Unauthorized — server requires OAuth. {meta}".strip())
84
+ resp.raise_for_status()
85
+ return _parse_http(resp)
86
+
87
+ def notify(self, payload) -> None:
88
+ self._client.post(self.url, json=payload, headers=self._hdrs())
89
+
90
+ def close(self):
91
+ self._client.close()
92
+
93
+
94
+ class _StdioTransport:
95
+ """Newline-delimited JSON-RPC over a spawned process's stdin/stdout."""
96
+ def __init__(self, command, args, env, timeout):
97
+ guard_stdio() # RCE gate: never spawn a local process in hosted mode
98
+ self.proc = subprocess.Popen(
99
+ [command, *(args or [])], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
100
+ stderr=subprocess.DEVNULL, text=True, bufsize=1, env=env)
101
+
102
+ def request(self, payload) -> dict:
103
+ assert self.proc.stdin and self.proc.stdout
104
+ self.proc.stdin.write(json.dumps(payload) + "\n")
105
+ self.proc.stdin.flush()
106
+ want = payload.get("id")
107
+ for line in self.proc.stdout:
108
+ line = line.strip()
109
+ if not line:
110
+ continue
111
+ msg = json.loads(line)
112
+ if msg.get("id") == want:
113
+ return msg
114
+ raise MCPError("stdio server closed before replying")
115
+
116
+ def notify(self, payload) -> None:
117
+ assert self.proc.stdin
118
+ self.proc.stdin.write(json.dumps(payload) + "\n")
119
+ self.proc.stdin.flush()
120
+
121
+ def close(self):
122
+ try:
123
+ self.proc.terminate()
124
+ self.proc.wait(timeout=5)
125
+ except Exception:
126
+ self.proc.kill()
127
+
128
+
129
+ def _parse_http(resp: httpx.Response) -> dict:
130
+ ctype = resp.headers.get("content-type", "")
131
+ if ctype.startswith("text/event-stream"):
132
+ for line in resp.text.splitlines():
133
+ if line.startswith("data:"):
134
+ chunk = json.loads(line[5:].strip())
135
+ if isinstance(chunk, dict) and ("result" in chunk or "error" in chunk):
136
+ return chunk
137
+ raise MCPError("no JSON-RPC message in SSE stream")
138
+ return resp.json()
139
+
140
+
141
+ class MCPSession:
142
+ """A short-lived MCP session over one transport.
143
+
144
+ HTTP: MCPSession(url, headers=..., spec="auto"|"2025-11-25"|"2026-07-28", oauth=...)
145
+ stdio: MCPSession(command="python", args=["server.py"], env=...)
146
+ """
147
+
148
+ # Class-level defaults so a session built via __new__ (tests inject a fake transport)
149
+ # still has them.
150
+ _inited = False
151
+ _keep_open = False
152
+
153
+ def __init__(self, url: str | None = None, headers: dict | None = None, timeout: float = 30, *,
154
+ command: str | None = None, args: list | None = None, env: dict | None = None,
155
+ spec: str = "auto", oauth: dict | None = None):
156
+ headers = dict(headers or {})
157
+ if oauth:
158
+ headers.setdefault("Authorization", f"Bearer {_fetch_oauth_token(oauth)}")
159
+ self.spec = spec
160
+ self._inited = False
161
+ self._keep_open = False
162
+ self._stdio = command is not None
163
+ if self._stdio:
164
+ self._t = _StdioTransport(command, args, env, timeout)
165
+ self._stateful = True # stdio is always a stateful session
166
+ else:
167
+ if not url:
168
+ raise MCPError("MCP session needs a url (HTTP) or command (stdio)")
169
+ self._stateful = spec != _STATELESS
170
+ self._t = _HTTPTransport(url, headers, timeout, self._stateful)
171
+
172
+ def _rpc(self, method: str, params: dict | None = None) -> dict:
173
+ payload = {"jsonrpc": "2.0", "id": _next_id(), "method": method, "params": params or {}}
174
+ data = self._t.request(payload)
175
+ if "error" in data:
176
+ raise MCPError(str(data["error"].get("message", "MCP error")))
177
+ return data.get("result", {})
178
+
179
+ def open(self) -> "MCPSession":
180
+ """Keep the transport open across calls until close(). list_tools()/call_tool()
181
+ otherwise close after each call, so a tool-calling loop would re-handshake every
182
+ time — and for stdio, spawn and kill a subprocess every time."""
183
+ self._keep_open = True
184
+ return self
185
+
186
+ def close(self) -> None:
187
+ self._keep_open = False
188
+ self._close()
189
+
190
+ @contextlib.contextmanager
191
+ def session(self):
192
+ """open() for the duration of a block. The session is spent afterwards, exactly as
193
+ it is after a bare call today."""
194
+ try:
195
+ yield self.open()
196
+ finally:
197
+ self.close()
198
+
199
+ def _close(self) -> None:
200
+ self._inited = False
201
+ self._t.close()
202
+
203
+ def _close_unless_kept(self) -> None:
204
+ if not self._keep_open:
205
+ self._close()
206
+
207
+ def _init(self) -> None:
208
+ if self._inited or not self._stateful:
209
+ return # stateless generation: no handshake, no session id
210
+ try:
211
+ self._rpc("initialize", {
212
+ "protocolVersion": _STATEFUL if self.spec == "auto" else self.spec,
213
+ "capabilities": {},
214
+ "clientInfo": {"name": "provekit", "version": "0.1.0"},
215
+ })
216
+ self._t.notify({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}})
217
+ self._inited = True
218
+ except Exception:
219
+ # auto: a server that rejects initialize is treated as stateless.
220
+ if self.spec == "auto":
221
+ self._stateful = False
222
+
223
+ def _paginate(self, method: str, key: str) -> list[dict]:
224
+ items, cursor = [], None
225
+ while True:
226
+ res = self._rpc(method, {"cursor": cursor} if cursor else {})
227
+ items += res.get(key, [])
228
+ cursor = res.get("nextCursor")
229
+ if not cursor:
230
+ return items
231
+
232
+ def list_tools(self) -> list[dict]:
233
+ try:
234
+ self._init()
235
+ tools = self._paginate("tools/list", "tools")
236
+ return [{"name": t["name"], "description": t.get("description", ""),
237
+ "input_schema": t.get("inputSchema") or {}} for t in tools]
238
+ finally:
239
+ self._close_unless_kept()
240
+
241
+ def list_resources(self) -> list[dict]:
242
+ try:
243
+ self._init()
244
+ res = self._paginate("resources/list", "resources")
245
+ return [{"uri": r.get("uri"), "name": r.get("name", ""),
246
+ "mime_type": r.get("mimeType", "")} for r in res]
247
+ finally:
248
+ self._close_unless_kept()
249
+
250
+ def list_prompts(self) -> list[dict]:
251
+ try:
252
+ self._init()
253
+ pr = self._paginate("prompts/list", "prompts")
254
+ return [{"name": p["name"], "description": p.get("description", ""),
255
+ "arguments": p.get("arguments", [])} for p in pr]
256
+ finally:
257
+ self._close_unless_kept()
258
+
259
+ def call_tool(self, name: str, args: dict) -> dict:
260
+ try:
261
+ self._init()
262
+ res = self._rpc("tools/call", {"name": name, "arguments": args or {}})
263
+ finally:
264
+ self._close_unless_kept()
265
+ if res.get("isError"):
266
+ raise MCPError(_text(res) or f"tool '{name}' failed")
267
+ return _unwrap(res)
268
+
269
+
270
+ def _text(res: dict) -> str | None:
271
+ for block in res.get("content", []):
272
+ if block.get("type") == "text":
273
+ return block.get("text")
274
+ return None
275
+
276
+
277
+ def _unwrap(res: dict):
278
+ sc = res.get("structuredContent")
279
+ if sc is not None:
280
+ if isinstance(sc, dict) and set(sc.keys()) == {"result"}:
281
+ return sc["result"]
282
+ return sc
283
+ txt = _text(res)
284
+ if txt is None:
285
+ return res.get("content", res)
286
+ try:
287
+ return json.loads(txt)
288
+ except json.JSONDecodeError:
289
+ return {"text": txt}
@@ -0,0 +1,93 @@
1
+ """Storage for paused flow-run contexts (breakpoint/step debugging).
2
+
3
+ In-memory by default (single-process local use). When REDIS_URL is set, contexts live in
4
+ Redis so step-debugging survives across uvicorn workers and restarts — pop uses GETDEL so
5
+ two concurrent /continue calls can't resume the same run twice.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from functools import lru_cache
12
+
13
+ from ..config import get_settings
14
+
15
+ _TTL = 1800 # evict paused runs abandoned for 30 min
16
+ _MAX = 200 # in-memory hard cap
17
+ _MAX_BYTES = 1_000_000 # reject a ctx that serializes larger than ~1MB
18
+
19
+
20
+ class _MemoryStore:
21
+ def __init__(self):
22
+ self._d: dict[str, dict] = {}
23
+ self._ts: dict[str, float] = {}
24
+
25
+ def store(self, rid: str, ctx: dict) -> None:
26
+ now = time.monotonic()
27
+ self._d[rid] = ctx
28
+ self._ts[rid] = now
29
+ for k in [k for k, t in list(self._ts.items()) if now - t > _TTL]:
30
+ self._d.pop(k, None); self._ts.pop(k, None)
31
+ if len(self._d) > _MAX:
32
+ for k in sorted(self._ts, key=self._ts.get)[: len(self._d) - _MAX]:
33
+ self._d.pop(k, None); self._ts.pop(k, None)
34
+
35
+ def get(self, rid: str) -> dict | None:
36
+ return self._d.get(rid)
37
+
38
+ def pop(self, rid: str) -> dict | None:
39
+ self._ts.pop(rid, None)
40
+ return self._d.pop(rid, None)
41
+
42
+ def drop(self, rid: str) -> None:
43
+ self._d.pop(rid, None); self._ts.pop(rid, None)
44
+
45
+
46
+ class _RedisStore:
47
+ _PREFIX = "agm:run:"
48
+
49
+ def __init__(self, url: str):
50
+ import redis # imported only when REDIS_URL is set
51
+ self._r = redis.Redis.from_url(url, decode_responses=True)
52
+
53
+ def _k(self, rid: str) -> str:
54
+ return self._PREFIX + rid
55
+
56
+ def store(self, rid: str, ctx: dict) -> None:
57
+ blob = json.dumps(ctx)
58
+ if len(blob) > _MAX_BYTES:
59
+ raise ValueError("run context too large to persist")
60
+ self._r.setex(self._k(rid), _TTL, blob)
61
+
62
+ def get(self, rid: str) -> dict | None:
63
+ blob = self._r.get(self._k(rid))
64
+ return json.loads(blob) if blob else None
65
+
66
+ def pop(self, rid: str) -> dict | None:
67
+ blob = self._r.getdel(self._k(rid)) # atomic — prevents double-resume
68
+ return json.loads(blob) if blob else None
69
+
70
+ def drop(self, rid: str) -> None:
71
+ self._r.delete(self._k(rid))
72
+
73
+
74
+ @lru_cache
75
+ def _store():
76
+ url = get_settings().redis_url
77
+ return _RedisStore(url) if url else _MemoryStore()
78
+
79
+
80
+ def store_ctx(rid: str, ctx: dict) -> None:
81
+ _store().store(rid, ctx)
82
+
83
+
84
+ def get_ctx(rid: str) -> dict | None:
85
+ return _store().get(rid)
86
+
87
+
88
+ def pop_ctx(rid: str) -> dict | None:
89
+ return _store().pop(rid)
90
+
91
+
92
+ def drop_ctx(rid: str) -> None:
93
+ _store().drop(rid)
@@ -0,0 +1,106 @@
1
+ """Encrypt connection secrets at rest (Fernet). Sealed values carry an "enc:" prefix;
2
+ plaintext values from older databases are accepted on read and upgraded by the
3
+ one-shot reseal pass at startup (and on any subsequent write).
4
+
5
+ Key source: SECRET_KEY env var (any string — derived to a Fernet key), or, for local
6
+ SQLite use, an auto-generated key file stored next to the database (.provekit.key).
7
+ Rotation: set the new SECRET_KEY, then re-enter credentials (values sealed with the
8
+ old key decrypt to "" with a logged warning rather than crashing).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import hashlib
14
+ import logging
15
+ import os
16
+ from functools import lru_cache
17
+ from pathlib import Path
18
+
19
+ from cryptography.fernet import Fernet, InvalidToken
20
+
21
+ from ..config import get_settings
22
+ from .masking import is_secret_header
23
+
24
+ _PREFIX = "enc:"
25
+ log = logging.getLogger("provekit.sealing")
26
+
27
+
28
+ def _key_file() -> Path | None:
29
+ url = get_settings().database_url
30
+ if url.startswith("sqlite:///"):
31
+ db_path = Path(url.removeprefix("sqlite:///"))
32
+ parent = db_path.parent if str(db_path.parent) else Path(".")
33
+ return parent / ".provekit.key"
34
+ return None
35
+
36
+
37
+ @lru_cache
38
+ def _fernet() -> Fernet:
39
+ s = get_settings()
40
+ if s.secret_key:
41
+ return Fernet(base64.urlsafe_b64encode(hashlib.sha256(s.secret_key.encode()).digest()))
42
+ kf = _key_file()
43
+ if kf is None:
44
+ raise RuntimeError("SECRET_KEY must be set when not using a local SQLite database")
45
+ if kf.exists():
46
+ return Fernet(kf.read_bytes().strip())
47
+ key = Fernet.generate_key()
48
+ kf.parent.mkdir(parents=True, exist_ok=True)
49
+ kf.write_bytes(key)
50
+ try:
51
+ os.chmod(kf, 0o600)
52
+ except OSError:
53
+ pass
54
+ log.info("generated local encryption key at %s", kf)
55
+ return Fernet(key)
56
+
57
+
58
+ def seal(value: str) -> str:
59
+ if not value or not isinstance(value, str) or value.startswith(_PREFIX):
60
+ return value
61
+ return _PREFIX + _fernet().encrypt(value.encode()).decode()
62
+
63
+
64
+ def unseal(value):
65
+ if not isinstance(value, str) or not value.startswith(_PREFIX):
66
+ return value
67
+ try:
68
+ return _fernet().decrypt(value[len(_PREFIX):].encode()).decode()
69
+ except (InvalidToken, ValueError):
70
+ log.warning("could not decrypt a stored secret (SECRET_KEY changed?) — treating as unset")
71
+ return ""
72
+
73
+
74
+ def seal_config(cfg: dict) -> dict:
75
+ """Encrypt the secret fields of a connection config: api_key, secret headers,
76
+ the MCP OAuth client_secret, and stdio env values (which routinely carry tokens)."""
77
+ out = dict(cfg)
78
+ if out.get("api_key"):
79
+ out["api_key"] = seal(out["api_key"])
80
+ hdrs = out.get("headers")
81
+ if isinstance(hdrs, dict):
82
+ out["headers"] = {k: (seal(v) if is_secret_header(k) and isinstance(v, str) and v else v)
83
+ for k, v in hdrs.items()}
84
+ oauth = out.get("oauth")
85
+ if isinstance(oauth, dict) and oauth.get("client_secret"):
86
+ out["oauth"] = {**oauth, "client_secret": seal(oauth["client_secret"])}
87
+ env = out.get("env")
88
+ if isinstance(env, dict):
89
+ out["env"] = {k: (seal(v) if isinstance(v, str) and v else v) for k, v in env.items()}
90
+ return out
91
+
92
+
93
+ def unseal_config(cfg: dict) -> dict:
94
+ out = dict(cfg)
95
+ if out.get("api_key"):
96
+ out["api_key"] = unseal(out["api_key"])
97
+ hdrs = out.get("headers")
98
+ if isinstance(hdrs, dict):
99
+ out["headers"] = {k: unseal(v) for k, v in hdrs.items()}
100
+ oauth = out.get("oauth")
101
+ if isinstance(oauth, dict) and "client_secret" in oauth:
102
+ out["oauth"] = {**oauth, "client_secret": unseal(oauth.get("client_secret"))}
103
+ env = out.get("env")
104
+ if isinstance(env, dict):
105
+ out["env"] = {k: unseal(v) for k, v in env.items()}
106
+ return out
@@ -0,0 +1,69 @@
1
+ """Bundled flow templates — the searchable gallery served to the New-flow picker."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import pathlib
6
+ from functools import lru_cache
7
+
8
+ from . import testfile
9
+
10
+ _DIR = pathlib.Path(__file__).resolve().parent.parent / "templates" / "flows"
11
+
12
+ # A small, hand-picked showcase surfaced at the top of the New-flow picker. The gallery is a
13
+ # regular 24-domain × 18-pattern grid (432 entries), which reads as a wall of near-duplicates;
14
+ # these are eight *distinct patterns* across eight *distinct domains* — each a different
15
+ # capability (branching, self-critique, structured output, safety gate, numeric gate,
16
+ # language routing) — so a newcomer sees a few great starting points instead of everything.
17
+ _FEATURED = [
18
+ "customer-support-triage-route",
19
+ "sales-lead-draft-critique-revise",
20
+ "healthcare-intake-extract-to-json",
21
+ "content-moderation-moderation-gate",
22
+ "legal-intake-summarize-then-decide",
23
+ "e-commerce-sentiment-route",
24
+ "recruiting-score-gate",
25
+ "travel-language-route",
26
+ ]
27
+
28
+
29
+ @lru_cache
30
+ def _manifest() -> list[dict]:
31
+ f = _DIR / "_manifest.json"
32
+ return json.loads(f.read_text()) if f.exists() else []
33
+
34
+
35
+ def featured() -> list[dict]:
36
+ """The curated showcase, in order. Skips any slug missing from the manifest so a
37
+ renamed template can't break the picker."""
38
+ by_slug = {m["slug"]: m for m in _manifest()}
39
+ return [by_slug[s] for s in _FEATURED if s in by_slug]
40
+
41
+
42
+ def search(q: str = "", limit: int = 60) -> list[dict]:
43
+ """Filter templates by a case-insensitive substring over name/description/category."""
44
+ items = _manifest()
45
+ q = (q or "").strip().lower()
46
+ if q:
47
+ items = [m for m in items
48
+ if q in m["name"].lower() or q in m["description"].lower() or q in m["category"].lower()]
49
+ return items[:limit]
50
+
51
+
52
+ def categories() -> list[str]:
53
+ return sorted({m["category"] for m in _manifest()})
54
+
55
+
56
+ def total() -> int:
57
+ return len(_manifest())
58
+
59
+
60
+ def load(slug: str) -> dict | None:
61
+ """Return the parsed .provekit flow doc for a template slug (None if unknown)."""
62
+ # Reject separators/traversal (incl. Windows drive-relative "C:foo"), then confirm the
63
+ # resolved path stays inside the templates dir before reading.
64
+ if not slug or any(ch in slug for ch in ("/", "\\", ":")) or ".." in slug:
65
+ return None
66
+ f = (_DIR / f"{slug}.yaml").resolve()
67
+ if _DIR not in f.parents or not f.exists():
68
+ return None
69
+ return testfile.load(f.read_text())
@@ -0,0 +1,122 @@
1
+ """The .provekit file format — plain-text, git-diffable tests and flows.
2
+
3
+ A *test* file bundles one request (prompt | tool | agent), its assertions, and an
4
+ optional dataset of input rows — enough to re-run it anywhere, including CI.
5
+ A *flow* file carries a visual flow's graph. Connections are referenced BY NAME:
6
+ credentials never enter the file; they resolve against the local workspace at
7
+ import/run time.
8
+
9
+ Canonical test shape (YAML, stable field order):
10
+
11
+ version: 1
12
+ kind: test
13
+ name: Support bot regression
14
+ connection: OpenAI (prod)
15
+ request:
16
+ type: prompt
17
+ model: gpt-4o-mini
18
+ system: You are a support assistant.
19
+ user: "{{message}}"
20
+ assertions:
21
+ - type: contains
22
+ value: refund
23
+ dataset:
24
+ - name: angry customer
25
+ variables: {message: "URGENT: refund now"}
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import yaml
30
+
31
+ VERSION = 1
32
+ KINDS = ("test", "flow")
33
+
34
+ # Stable, human-friendly field order per request type.
35
+ _REQ_ORDER = {
36
+ "prompt": ["type", "model", "system", "user", "messages", "temperature", "max_tokens"],
37
+ "tool": ["type", "tool", "args"],
38
+ "agent": ["type", "method", "path", "headers", "body", "stream"],
39
+ }
40
+ _ASSERT_ORDER = ["type", "name", "path", "value", "schema", "criteria", "model"]
41
+
42
+ # Never serialized: secrets and workspace-local ids.
43
+ _STRIP = {"api_key", "connection_id", "assertions", "_k", "provider", "base_url", "url"}
44
+
45
+
46
+ def _ordered(d: dict, order: list[str]) -> dict:
47
+ out = {k: d[k] for k in order if k in d and d[k] not in (None, "", {}, [])}
48
+ out.update({k: v for k, v in d.items() if k not in out and k not in order})
49
+ return out
50
+
51
+
52
+ def _clean_request(request: dict) -> dict:
53
+ req = {k: v for k, v in request.items() if k not in _STRIP}
54
+ return _ordered(req, _REQ_ORDER.get(request.get("type", ""), ["type"]))
55
+
56
+
57
+ def dump_test(name: str, request: dict, connection_name: str | None = None,
58
+ dataset: list[dict] | None = None) -> str:
59
+ doc: dict = {"version": VERSION, "kind": "test", "name": name or "test"}
60
+ if connection_name:
61
+ doc["connection"] = connection_name
62
+ doc["request"] = _clean_request(request)
63
+ asserts = request.get("assertions") or []
64
+ if asserts:
65
+ doc["assertions"] = [_ordered(dict(a), _ASSERT_ORDER) for a in asserts]
66
+ if dataset:
67
+ doc["dataset"] = [{"name": r.get("name") or f"row {i + 1}", "variables": r.get("variables") or {}}
68
+ for i, r in enumerate(dataset)]
69
+ return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
70
+
71
+
72
+ def dump_flow(name: str, description: str, nodes: list, edges: list,
73
+ connection_names: dict[int, str] | None = None) -> str:
74
+ """connection_names maps connection_id -> name so flow files stay portable."""
75
+ names = connection_names or {}
76
+ out_nodes = []
77
+ for n in nodes or []:
78
+ node = dict(n)
79
+ cfg = dict(node.get("config") or {})
80
+ cid = cfg.pop("connection_id", None)
81
+ if cid is not None and cid in names:
82
+ cfg["connection"] = names[cid]
83
+ node["config"] = cfg
84
+ out_nodes.append(node)
85
+ doc: dict = {"version": VERSION, "kind": "flow", "name": name or "flow"}
86
+ if description:
87
+ doc["description"] = description
88
+ doc["nodes"] = out_nodes
89
+ doc["edges"] = edges or []
90
+ return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
91
+
92
+
93
+ def load(text: str) -> dict:
94
+ """Parse + validate an .provekit document. Returns the normalized dict; the caller
95
+ resolves `connection` names to ids against its own workspace."""
96
+ try:
97
+ doc = yaml.safe_load(text)
98
+ except yaml.YAMLError as exc:
99
+ raise ValueError(f"not valid YAML: {exc}") from exc
100
+ if not isinstance(doc, dict):
101
+ raise ValueError("expected a YAML mapping at the top level")
102
+ if doc.get("version") != VERSION:
103
+ raise ValueError(f"unsupported version {doc.get('version')!r} (expected {VERSION})")
104
+ kind = doc.get("kind")
105
+ if kind not in KINDS:
106
+ raise ValueError(f"unsupported kind {kind!r} (expected one of {KINDS})")
107
+ if kind == "test":
108
+ req = doc.get("request")
109
+ if not isinstance(req, dict) or req.get("type") not in ("prompt", "tool", "agent"):
110
+ raise ValueError("test needs a request with type prompt | tool | agent")
111
+ if "api_key" in req:
112
+ raise ValueError("credentials do not belong in .provekit files — use a connection name")
113
+ for a in doc.get("assertions") or []:
114
+ if not isinstance(a, dict) or not a.get("type"):
115
+ raise ValueError("each assertion needs a type")
116
+ for r in doc.get("dataset") or []:
117
+ if not isinstance(r, dict):
118
+ raise ValueError("each dataset row must be a mapping")
119
+ else: # flow
120
+ if not isinstance(doc.get("nodes"), list) or not isinstance(doc.get("edges"), list):
121
+ raise ValueError("flow needs nodes and edges lists")
122
+ return doc