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.
- provekit/__init__.py +0 -0
- provekit/cli.py +256 -0
- provekit/config.py +77 -0
- provekit/database.py +132 -0
- provekit/main.py +115 -0
- provekit/migrations/env.py +40 -0
- provekit/migrations/script.py.mako +22 -0
- provekit/migrations/versions/323eb73d463c_user_email_verified.py +32 -0
- provekit/migrations/versions/49e8ab812556_baseline_schema.py +241 -0
- provekit/migrations/versions/819ed5ff183e_workspace_ingest_key.py +35 -0
- provekit/migrations/versions/a1b2c3d4e5f6_user_token_version.py +27 -0
- provekit/models.py +191 -0
- provekit/observability.py +154 -0
- provekit/routers/__init__.py +0 -0
- provekit/routers/auth.py +150 -0
- provekit/routers/connections.py +270 -0
- provekit/routers/deployments.py +161 -0
- provekit/routers/flows.py +188 -0
- provekit/routers/library.py +265 -0
- provekit/routers/prompts.py +72 -0
- provekit/routers/run.py +203 -0
- provekit/routers/runtime.py +103 -0
- provekit/routers/traces.py +65 -0
- provekit/routers/usage.py +42 -0
- provekit/services/__init__.py +0 -0
- provekit/services/assertions.py +135 -0
- provekit/services/auth.py +131 -0
- provekit/services/deploy.py +63 -0
- provekit/services/dispatch.py +313 -0
- provekit/services/email.py +38 -0
- provekit/services/flow.py +299 -0
- provekit/services/limits.py +109 -0
- provekit/services/masking.py +45 -0
- provekit/services/netguard.py +69 -0
- provekit/services/otel.py +146 -0
- provekit/services/promptfoo.py +92 -0
- provekit/services/providers/__init__.py +0 -0
- provekit/services/providers/a2a_client.py +110 -0
- provekit/services/providers/agent_http.py +55 -0
- provekit/services/providers/llm.py +364 -0
- provekit/services/providers/mcp_client.py +289 -0
- provekit/services/runstore.py +93 -0
- provekit/services/sealing.py +106 -0
- provekit/services/templates.py +69 -0
- provekit/services/testfile.py +122 -0
- provekit/services/tooling.py +194 -0
- provekit/services/workspace.py +91 -0
- provekit/templates/flows/_manifest.json +2594 -0
- provekit/templates/flows/banking-action-items.yaml +43 -0
- provekit/templates/flows/banking-answer.yaml +43 -0
- provekit/templates/flows/banking-classify-intent.yaml +43 -0
- provekit/templates/flows/banking-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/banking-extract-to-json.yaml +43 -0
- provekit/templates/flows/banking-faq-match-answer.yaml +60 -0
- provekit/templates/flows/banking-keyword-extract.yaml +43 -0
- provekit/templates/flows/banking-language-route.yaml +103 -0
- provekit/templates/flows/banking-moderation-gate.yaml +73 -0
- provekit/templates/flows/banking-redact-summarize.yaml +59 -0
- provekit/templates/flows/banking-score-gate.yaml +73 -0
- provekit/templates/flows/banking-sentiment-route.yaml +73 -0
- provekit/templates/flows/banking-severity-tiers.yaml +103 -0
- provekit/templates/flows/banking-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/banking-tag-enrich.yaml +43 -0
- provekit/templates/flows/banking-tone-rewrite.yaml +43 -0
- provekit/templates/flows/banking-translate-reply.yaml +58 -0
- provekit/templates/flows/banking-triage-route.yaml +73 -0
- provekit/templates/flows/community-forum-action-items.yaml +43 -0
- provekit/templates/flows/community-forum-answer.yaml +43 -0
- provekit/templates/flows/community-forum-classify-intent.yaml +43 -0
- provekit/templates/flows/community-forum-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/community-forum-extract-to-json.yaml +43 -0
- provekit/templates/flows/community-forum-faq-match-answer.yaml +60 -0
- provekit/templates/flows/community-forum-keyword-extract.yaml +43 -0
- provekit/templates/flows/community-forum-language-route.yaml +103 -0
- provekit/templates/flows/community-forum-moderation-gate.yaml +73 -0
- provekit/templates/flows/community-forum-redact-summarize.yaml +59 -0
- provekit/templates/flows/community-forum-score-gate.yaml +73 -0
- provekit/templates/flows/community-forum-sentiment-route.yaml +73 -0
- provekit/templates/flows/community-forum-severity-tiers.yaml +103 -0
- provekit/templates/flows/community-forum-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/community-forum-tag-enrich.yaml +43 -0
- provekit/templates/flows/community-forum-tone-rewrite.yaml +43 -0
- provekit/templates/flows/community-forum-translate-reply.yaml +58 -0
- provekit/templates/flows/community-forum-triage-route.yaml +73 -0
- provekit/templates/flows/content-moderation-action-items.yaml +43 -0
- provekit/templates/flows/content-moderation-answer.yaml +43 -0
- provekit/templates/flows/content-moderation-classify-intent.yaml +43 -0
- provekit/templates/flows/content-moderation-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/content-moderation-extract-to-json.yaml +43 -0
- provekit/templates/flows/content-moderation-faq-match-answer.yaml +60 -0
- provekit/templates/flows/content-moderation-keyword-extract.yaml +43 -0
- provekit/templates/flows/content-moderation-language-route.yaml +103 -0
- provekit/templates/flows/content-moderation-moderation-gate.yaml +73 -0
- provekit/templates/flows/content-moderation-redact-summarize.yaml +59 -0
- provekit/templates/flows/content-moderation-score-gate.yaml +73 -0
- provekit/templates/flows/content-moderation-sentiment-route.yaml +73 -0
- provekit/templates/flows/content-moderation-severity-tiers.yaml +103 -0
- provekit/templates/flows/content-moderation-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/content-moderation-tag-enrich.yaml +43 -0
- provekit/templates/flows/content-moderation-tone-rewrite.yaml +43 -0
- provekit/templates/flows/content-moderation-translate-reply.yaml +58 -0
- provekit/templates/flows/content-moderation-triage-route.yaml +73 -0
- provekit/templates/flows/customer-support-action-items.yaml +43 -0
- provekit/templates/flows/customer-support-answer.yaml +43 -0
- provekit/templates/flows/customer-support-classify-intent.yaml +43 -0
- provekit/templates/flows/customer-support-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/customer-support-extract-to-json.yaml +43 -0
- provekit/templates/flows/customer-support-faq-match-answer.yaml +60 -0
- provekit/templates/flows/customer-support-keyword-extract.yaml +43 -0
- provekit/templates/flows/customer-support-language-route.yaml +103 -0
- provekit/templates/flows/customer-support-moderation-gate.yaml +73 -0
- provekit/templates/flows/customer-support-redact-summarize.yaml +59 -0
- provekit/templates/flows/customer-support-score-gate.yaml +73 -0
- provekit/templates/flows/customer-support-sentiment-route.yaml +73 -0
- provekit/templates/flows/customer-support-severity-tiers.yaml +103 -0
- provekit/templates/flows/customer-support-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/customer-support-tag-enrich.yaml +43 -0
- provekit/templates/flows/customer-support-tone-rewrite.yaml +43 -0
- provekit/templates/flows/customer-support-translate-reply.yaml +58 -0
- provekit/templates/flows/customer-support-triage-route.yaml +73 -0
- provekit/templates/flows/developer-tooling-action-items.yaml +43 -0
- provekit/templates/flows/developer-tooling-answer.yaml +43 -0
- provekit/templates/flows/developer-tooling-classify-intent.yaml +43 -0
- provekit/templates/flows/developer-tooling-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/developer-tooling-extract-to-json.yaml +43 -0
- provekit/templates/flows/developer-tooling-faq-match-answer.yaml +60 -0
- provekit/templates/flows/developer-tooling-keyword-extract.yaml +43 -0
- provekit/templates/flows/developer-tooling-language-route.yaml +103 -0
- provekit/templates/flows/developer-tooling-moderation-gate.yaml +73 -0
- provekit/templates/flows/developer-tooling-redact-summarize.yaml +59 -0
- provekit/templates/flows/developer-tooling-score-gate.yaml +73 -0
- provekit/templates/flows/developer-tooling-sentiment-route.yaml +73 -0
- provekit/templates/flows/developer-tooling-severity-tiers.yaml +103 -0
- provekit/templates/flows/developer-tooling-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/developer-tooling-tag-enrich.yaml +43 -0
- provekit/templates/flows/developer-tooling-tone-rewrite.yaml +43 -0
- provekit/templates/flows/developer-tooling-translate-reply.yaml +58 -0
- provekit/templates/flows/developer-tooling-triage-route.yaml +73 -0
- provekit/templates/flows/devrel-action-items.yaml +43 -0
- provekit/templates/flows/devrel-answer.yaml +43 -0
- provekit/templates/flows/devrel-classify-intent.yaml +43 -0
- provekit/templates/flows/devrel-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/devrel-extract-to-json.yaml +43 -0
- provekit/templates/flows/devrel-faq-match-answer.yaml +60 -0
- provekit/templates/flows/devrel-keyword-extract.yaml +43 -0
- provekit/templates/flows/devrel-language-route.yaml +103 -0
- provekit/templates/flows/devrel-moderation-gate.yaml +73 -0
- provekit/templates/flows/devrel-redact-summarize.yaml +59 -0
- provekit/templates/flows/devrel-score-gate.yaml +73 -0
- provekit/templates/flows/devrel-sentiment-route.yaml +73 -0
- provekit/templates/flows/devrel-severity-tiers.yaml +103 -0
- provekit/templates/flows/devrel-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/devrel-tag-enrich.yaml +43 -0
- provekit/templates/flows/devrel-tone-rewrite.yaml +43 -0
- provekit/templates/flows/devrel-translate-reply.yaml +58 -0
- provekit/templates/flows/devrel-triage-route.yaml +73 -0
- provekit/templates/flows/e-commerce-action-items.yaml +43 -0
- provekit/templates/flows/e-commerce-answer.yaml +43 -0
- provekit/templates/flows/e-commerce-classify-intent.yaml +43 -0
- provekit/templates/flows/e-commerce-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/e-commerce-extract-to-json.yaml +43 -0
- provekit/templates/flows/e-commerce-faq-match-answer.yaml +60 -0
- provekit/templates/flows/e-commerce-keyword-extract.yaml +43 -0
- provekit/templates/flows/e-commerce-language-route.yaml +103 -0
- provekit/templates/flows/e-commerce-moderation-gate.yaml +73 -0
- provekit/templates/flows/e-commerce-redact-summarize.yaml +59 -0
- provekit/templates/flows/e-commerce-score-gate.yaml +73 -0
- provekit/templates/flows/e-commerce-sentiment-route.yaml +73 -0
- provekit/templates/flows/e-commerce-severity-tiers.yaml +103 -0
- provekit/templates/flows/e-commerce-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/e-commerce-tag-enrich.yaml +43 -0
- provekit/templates/flows/e-commerce-tone-rewrite.yaml +43 -0
- provekit/templates/flows/e-commerce-translate-reply.yaml +58 -0
- provekit/templates/flows/e-commerce-triage-route.yaml +73 -0
- provekit/templates/flows/education-action-items.yaml +43 -0
- provekit/templates/flows/education-answer.yaml +43 -0
- provekit/templates/flows/education-classify-intent.yaml +43 -0
- provekit/templates/flows/education-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/education-extract-to-json.yaml +43 -0
- provekit/templates/flows/education-faq-match-answer.yaml +60 -0
- provekit/templates/flows/education-keyword-extract.yaml +43 -0
- provekit/templates/flows/education-language-route.yaml +103 -0
- provekit/templates/flows/education-moderation-gate.yaml +73 -0
- provekit/templates/flows/education-redact-summarize.yaml +59 -0
- provekit/templates/flows/education-score-gate.yaml +73 -0
- provekit/templates/flows/education-sentiment-route.yaml +73 -0
- provekit/templates/flows/education-severity-tiers.yaml +103 -0
- provekit/templates/flows/education-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/education-tag-enrich.yaml +43 -0
- provekit/templates/flows/education-tone-rewrite.yaml +43 -0
- provekit/templates/flows/education-translate-reply.yaml +58 -0
- provekit/templates/flows/education-triage-route.yaml +73 -0
- provekit/templates/flows/finance-action-items.yaml +43 -0
- provekit/templates/flows/finance-answer.yaml +43 -0
- provekit/templates/flows/finance-classify-intent.yaml +43 -0
- provekit/templates/flows/finance-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/finance-extract-to-json.yaml +43 -0
- provekit/templates/flows/finance-faq-match-answer.yaml +60 -0
- provekit/templates/flows/finance-keyword-extract.yaml +43 -0
- provekit/templates/flows/finance-language-route.yaml +103 -0
- provekit/templates/flows/finance-moderation-gate.yaml +73 -0
- provekit/templates/flows/finance-redact-summarize.yaml +59 -0
- provekit/templates/flows/finance-score-gate.yaml +73 -0
- provekit/templates/flows/finance-sentiment-route.yaml +73 -0
- provekit/templates/flows/finance-severity-tiers.yaml +103 -0
- provekit/templates/flows/finance-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/finance-tag-enrich.yaml +43 -0
- provekit/templates/flows/finance-tone-rewrite.yaml +43 -0
- provekit/templates/flows/finance-translate-reply.yaml +58 -0
- provekit/templates/flows/finance-triage-route.yaml +73 -0
- provekit/templates/flows/food-delivery-action-items.yaml +43 -0
- provekit/templates/flows/food-delivery-answer.yaml +43 -0
- provekit/templates/flows/food-delivery-classify-intent.yaml +43 -0
- provekit/templates/flows/food-delivery-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/food-delivery-extract-to-json.yaml +43 -0
- provekit/templates/flows/food-delivery-faq-match-answer.yaml +60 -0
- provekit/templates/flows/food-delivery-keyword-extract.yaml +43 -0
- provekit/templates/flows/food-delivery-language-route.yaml +103 -0
- provekit/templates/flows/food-delivery-moderation-gate.yaml +73 -0
- provekit/templates/flows/food-delivery-redact-summarize.yaml +59 -0
- provekit/templates/flows/food-delivery-score-gate.yaml +73 -0
- provekit/templates/flows/food-delivery-sentiment-route.yaml +73 -0
- provekit/templates/flows/food-delivery-severity-tiers.yaml +103 -0
- provekit/templates/flows/food-delivery-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/food-delivery-tag-enrich.yaml +43 -0
- provekit/templates/flows/food-delivery-tone-rewrite.yaml +43 -0
- provekit/templates/flows/food-delivery-translate-reply.yaml +58 -0
- provekit/templates/flows/food-delivery-triage-route.yaml +73 -0
- provekit/templates/flows/gaming-support-action-items.yaml +43 -0
- provekit/templates/flows/gaming-support-answer.yaml +43 -0
- provekit/templates/flows/gaming-support-classify-intent.yaml +43 -0
- provekit/templates/flows/gaming-support-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/gaming-support-extract-to-json.yaml +43 -0
- provekit/templates/flows/gaming-support-faq-match-answer.yaml +60 -0
- provekit/templates/flows/gaming-support-keyword-extract.yaml +43 -0
- provekit/templates/flows/gaming-support-language-route.yaml +103 -0
- provekit/templates/flows/gaming-support-moderation-gate.yaml +73 -0
- provekit/templates/flows/gaming-support-redact-summarize.yaml +59 -0
- provekit/templates/flows/gaming-support-score-gate.yaml +73 -0
- provekit/templates/flows/gaming-support-sentiment-route.yaml +73 -0
- provekit/templates/flows/gaming-support-severity-tiers.yaml +103 -0
- provekit/templates/flows/gaming-support-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/gaming-support-tag-enrich.yaml +43 -0
- provekit/templates/flows/gaming-support-tone-rewrite.yaml +43 -0
- provekit/templates/flows/gaming-support-translate-reply.yaml +58 -0
- provekit/templates/flows/gaming-support-triage-route.yaml +73 -0
- provekit/templates/flows/healthcare-intake-action-items.yaml +43 -0
- provekit/templates/flows/healthcare-intake-answer.yaml +43 -0
- provekit/templates/flows/healthcare-intake-classify-intent.yaml +43 -0
- provekit/templates/flows/healthcare-intake-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/healthcare-intake-extract-to-json.yaml +43 -0
- provekit/templates/flows/healthcare-intake-faq-match-answer.yaml +60 -0
- provekit/templates/flows/healthcare-intake-keyword-extract.yaml +43 -0
- provekit/templates/flows/healthcare-intake-language-route.yaml +103 -0
- provekit/templates/flows/healthcare-intake-moderation-gate.yaml +73 -0
- provekit/templates/flows/healthcare-intake-redact-summarize.yaml +59 -0
- provekit/templates/flows/healthcare-intake-score-gate.yaml +73 -0
- provekit/templates/flows/healthcare-intake-sentiment-route.yaml +73 -0
- provekit/templates/flows/healthcare-intake-severity-tiers.yaml +103 -0
- provekit/templates/flows/healthcare-intake-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/healthcare-intake-tag-enrich.yaml +43 -0
- provekit/templates/flows/healthcare-intake-tone-rewrite.yaml +43 -0
- provekit/templates/flows/healthcare-intake-translate-reply.yaml +58 -0
- provekit/templates/flows/healthcare-intake-triage-route.yaml +73 -0
- provekit/templates/flows/hr-action-items.yaml +43 -0
- provekit/templates/flows/hr-answer.yaml +43 -0
- provekit/templates/flows/hr-classify-intent.yaml +43 -0
- provekit/templates/flows/hr-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/hr-extract-to-json.yaml +43 -0
- provekit/templates/flows/hr-faq-match-answer.yaml +60 -0
- provekit/templates/flows/hr-keyword-extract.yaml +43 -0
- provekit/templates/flows/hr-language-route.yaml +103 -0
- provekit/templates/flows/hr-moderation-gate.yaml +73 -0
- provekit/templates/flows/hr-redact-summarize.yaml +59 -0
- provekit/templates/flows/hr-score-gate.yaml +73 -0
- provekit/templates/flows/hr-sentiment-route.yaml +73 -0
- provekit/templates/flows/hr-severity-tiers.yaml +103 -0
- provekit/templates/flows/hr-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/hr-tag-enrich.yaml +43 -0
- provekit/templates/flows/hr-tone-rewrite.yaml +43 -0
- provekit/templates/flows/hr-translate-reply.yaml +58 -0
- provekit/templates/flows/hr-triage-route.yaml +73 -0
- provekit/templates/flows/insurance-action-items.yaml +43 -0
- provekit/templates/flows/insurance-answer.yaml +43 -0
- provekit/templates/flows/insurance-classify-intent.yaml +43 -0
- provekit/templates/flows/insurance-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/insurance-extract-to-json.yaml +43 -0
- provekit/templates/flows/insurance-faq-match-answer.yaml +60 -0
- provekit/templates/flows/insurance-keyword-extract.yaml +43 -0
- provekit/templates/flows/insurance-language-route.yaml +103 -0
- provekit/templates/flows/insurance-moderation-gate.yaml +73 -0
- provekit/templates/flows/insurance-redact-summarize.yaml +59 -0
- provekit/templates/flows/insurance-score-gate.yaml +73 -0
- provekit/templates/flows/insurance-sentiment-route.yaml +73 -0
- provekit/templates/flows/insurance-severity-tiers.yaml +103 -0
- provekit/templates/flows/insurance-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/insurance-tag-enrich.yaml +43 -0
- provekit/templates/flows/insurance-tone-rewrite.yaml +43 -0
- provekit/templates/flows/insurance-translate-reply.yaml +58 -0
- provekit/templates/flows/insurance-triage-route.yaml +73 -0
- provekit/templates/flows/it-helpdesk-action-items.yaml +43 -0
- provekit/templates/flows/it-helpdesk-answer.yaml +43 -0
- provekit/templates/flows/it-helpdesk-classify-intent.yaml +43 -0
- provekit/templates/flows/it-helpdesk-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/it-helpdesk-extract-to-json.yaml +43 -0
- provekit/templates/flows/it-helpdesk-faq-match-answer.yaml +60 -0
- provekit/templates/flows/it-helpdesk-keyword-extract.yaml +43 -0
- provekit/templates/flows/it-helpdesk-language-route.yaml +103 -0
- provekit/templates/flows/it-helpdesk-moderation-gate.yaml +73 -0
- provekit/templates/flows/it-helpdesk-redact-summarize.yaml +59 -0
- provekit/templates/flows/it-helpdesk-score-gate.yaml +73 -0
- provekit/templates/flows/it-helpdesk-sentiment-route.yaml +73 -0
- provekit/templates/flows/it-helpdesk-severity-tiers.yaml +103 -0
- provekit/templates/flows/it-helpdesk-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/it-helpdesk-tag-enrich.yaml +43 -0
- provekit/templates/flows/it-helpdesk-tone-rewrite.yaml +43 -0
- provekit/templates/flows/it-helpdesk-translate-reply.yaml +58 -0
- provekit/templates/flows/it-helpdesk-triage-route.yaml +73 -0
- provekit/templates/flows/legal-intake-action-items.yaml +43 -0
- provekit/templates/flows/legal-intake-answer.yaml +43 -0
- provekit/templates/flows/legal-intake-classify-intent.yaml +43 -0
- provekit/templates/flows/legal-intake-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/legal-intake-extract-to-json.yaml +43 -0
- provekit/templates/flows/legal-intake-faq-match-answer.yaml +60 -0
- provekit/templates/flows/legal-intake-keyword-extract.yaml +43 -0
- provekit/templates/flows/legal-intake-language-route.yaml +103 -0
- provekit/templates/flows/legal-intake-moderation-gate.yaml +73 -0
- provekit/templates/flows/legal-intake-redact-summarize.yaml +59 -0
- provekit/templates/flows/legal-intake-score-gate.yaml +73 -0
- provekit/templates/flows/legal-intake-sentiment-route.yaml +73 -0
- provekit/templates/flows/legal-intake-severity-tiers.yaml +103 -0
- provekit/templates/flows/legal-intake-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/legal-intake-tag-enrich.yaml +43 -0
- provekit/templates/flows/legal-intake-tone-rewrite.yaml +43 -0
- provekit/templates/flows/legal-intake-translate-reply.yaml +58 -0
- provekit/templates/flows/legal-intake-triage-route.yaml +73 -0
- provekit/templates/flows/logistics-action-items.yaml +43 -0
- provekit/templates/flows/logistics-answer.yaml +43 -0
- provekit/templates/flows/logistics-classify-intent.yaml +43 -0
- provekit/templates/flows/logistics-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/logistics-extract-to-json.yaml +43 -0
- provekit/templates/flows/logistics-faq-match-answer.yaml +60 -0
- provekit/templates/flows/logistics-keyword-extract.yaml +43 -0
- provekit/templates/flows/logistics-language-route.yaml +103 -0
- provekit/templates/flows/logistics-moderation-gate.yaml +73 -0
- provekit/templates/flows/logistics-redact-summarize.yaml +59 -0
- provekit/templates/flows/logistics-score-gate.yaml +73 -0
- provekit/templates/flows/logistics-sentiment-route.yaml +73 -0
- provekit/templates/flows/logistics-severity-tiers.yaml +103 -0
- provekit/templates/flows/logistics-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/logistics-tag-enrich.yaml +43 -0
- provekit/templates/flows/logistics-tone-rewrite.yaml +43 -0
- provekit/templates/flows/logistics-translate-reply.yaml +58 -0
- provekit/templates/flows/logistics-triage-route.yaml +73 -0
- provekit/templates/flows/marketing-action-items.yaml +43 -0
- provekit/templates/flows/marketing-answer.yaml +43 -0
- provekit/templates/flows/marketing-classify-intent.yaml +43 -0
- provekit/templates/flows/marketing-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/marketing-extract-to-json.yaml +43 -0
- provekit/templates/flows/marketing-faq-match-answer.yaml +60 -0
- provekit/templates/flows/marketing-keyword-extract.yaml +43 -0
- provekit/templates/flows/marketing-language-route.yaml +103 -0
- provekit/templates/flows/marketing-moderation-gate.yaml +73 -0
- provekit/templates/flows/marketing-redact-summarize.yaml +59 -0
- provekit/templates/flows/marketing-score-gate.yaml +73 -0
- provekit/templates/flows/marketing-sentiment-route.yaml +73 -0
- provekit/templates/flows/marketing-severity-tiers.yaml +103 -0
- provekit/templates/flows/marketing-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/marketing-tag-enrich.yaml +43 -0
- provekit/templates/flows/marketing-tone-rewrite.yaml +43 -0
- provekit/templates/flows/marketing-translate-reply.yaml +58 -0
- provekit/templates/flows/marketing-triage-route.yaml +73 -0
- provekit/templates/flows/nonprofit-action-items.yaml +43 -0
- provekit/templates/flows/nonprofit-answer.yaml +43 -0
- provekit/templates/flows/nonprofit-classify-intent.yaml +43 -0
- provekit/templates/flows/nonprofit-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/nonprofit-extract-to-json.yaml +43 -0
- provekit/templates/flows/nonprofit-faq-match-answer.yaml +60 -0
- provekit/templates/flows/nonprofit-keyword-extract.yaml +43 -0
- provekit/templates/flows/nonprofit-language-route.yaml +103 -0
- provekit/templates/flows/nonprofit-moderation-gate.yaml +73 -0
- provekit/templates/flows/nonprofit-redact-summarize.yaml +59 -0
- provekit/templates/flows/nonprofit-score-gate.yaml +73 -0
- provekit/templates/flows/nonprofit-sentiment-route.yaml +73 -0
- provekit/templates/flows/nonprofit-severity-tiers.yaml +103 -0
- provekit/templates/flows/nonprofit-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/nonprofit-tag-enrich.yaml +43 -0
- provekit/templates/flows/nonprofit-tone-rewrite.yaml +43 -0
- provekit/templates/flows/nonprofit-translate-reply.yaml +58 -0
- provekit/templates/flows/nonprofit-triage-route.yaml +73 -0
- provekit/templates/flows/real-estate-action-items.yaml +43 -0
- provekit/templates/flows/real-estate-answer.yaml +43 -0
- provekit/templates/flows/real-estate-classify-intent.yaml +43 -0
- provekit/templates/flows/real-estate-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/real-estate-extract-to-json.yaml +43 -0
- provekit/templates/flows/real-estate-faq-match-answer.yaml +60 -0
- provekit/templates/flows/real-estate-keyword-extract.yaml +43 -0
- provekit/templates/flows/real-estate-language-route.yaml +103 -0
- provekit/templates/flows/real-estate-moderation-gate.yaml +73 -0
- provekit/templates/flows/real-estate-redact-summarize.yaml +59 -0
- provekit/templates/flows/real-estate-score-gate.yaml +73 -0
- provekit/templates/flows/real-estate-sentiment-route.yaml +73 -0
- provekit/templates/flows/real-estate-severity-tiers.yaml +103 -0
- provekit/templates/flows/real-estate-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/real-estate-tag-enrich.yaml +43 -0
- provekit/templates/flows/real-estate-tone-rewrite.yaml +43 -0
- provekit/templates/flows/real-estate-translate-reply.yaml +58 -0
- provekit/templates/flows/real-estate-triage-route.yaml +73 -0
- provekit/templates/flows/recruiting-action-items.yaml +43 -0
- provekit/templates/flows/recruiting-answer.yaml +43 -0
- provekit/templates/flows/recruiting-classify-intent.yaml +43 -0
- provekit/templates/flows/recruiting-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/recruiting-extract-to-json.yaml +43 -0
- provekit/templates/flows/recruiting-faq-match-answer.yaml +60 -0
- provekit/templates/flows/recruiting-keyword-extract.yaml +43 -0
- provekit/templates/flows/recruiting-language-route.yaml +103 -0
- provekit/templates/flows/recruiting-moderation-gate.yaml +73 -0
- provekit/templates/flows/recruiting-redact-summarize.yaml +59 -0
- provekit/templates/flows/recruiting-score-gate.yaml +73 -0
- provekit/templates/flows/recruiting-sentiment-route.yaml +73 -0
- provekit/templates/flows/recruiting-severity-tiers.yaml +103 -0
- provekit/templates/flows/recruiting-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/recruiting-tag-enrich.yaml +43 -0
- provekit/templates/flows/recruiting-tone-rewrite.yaml +43 -0
- provekit/templates/flows/recruiting-translate-reply.yaml +58 -0
- provekit/templates/flows/recruiting-triage-route.yaml +73 -0
- provekit/templates/flows/research-action-items.yaml +43 -0
- provekit/templates/flows/research-answer.yaml +43 -0
- provekit/templates/flows/research-classify-intent.yaml +43 -0
- provekit/templates/flows/research-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/research-extract-to-json.yaml +43 -0
- provekit/templates/flows/research-faq-match-answer.yaml +60 -0
- provekit/templates/flows/research-keyword-extract.yaml +43 -0
- provekit/templates/flows/research-language-route.yaml +103 -0
- provekit/templates/flows/research-moderation-gate.yaml +73 -0
- provekit/templates/flows/research-redact-summarize.yaml +59 -0
- provekit/templates/flows/research-score-gate.yaml +73 -0
- provekit/templates/flows/research-sentiment-route.yaml +73 -0
- provekit/templates/flows/research-severity-tiers.yaml +103 -0
- provekit/templates/flows/research-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/research-tag-enrich.yaml +43 -0
- provekit/templates/flows/research-tone-rewrite.yaml +43 -0
- provekit/templates/flows/research-translate-reply.yaml +58 -0
- provekit/templates/flows/research-triage-route.yaml +73 -0
- provekit/templates/flows/sales-lead-action-items.yaml +43 -0
- provekit/templates/flows/sales-lead-answer.yaml +43 -0
- provekit/templates/flows/sales-lead-classify-intent.yaml +43 -0
- provekit/templates/flows/sales-lead-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/sales-lead-extract-to-json.yaml +43 -0
- provekit/templates/flows/sales-lead-faq-match-answer.yaml +60 -0
- provekit/templates/flows/sales-lead-keyword-extract.yaml +43 -0
- provekit/templates/flows/sales-lead-language-route.yaml +103 -0
- provekit/templates/flows/sales-lead-moderation-gate.yaml +73 -0
- provekit/templates/flows/sales-lead-redact-summarize.yaml +59 -0
- provekit/templates/flows/sales-lead-score-gate.yaml +73 -0
- provekit/templates/flows/sales-lead-sentiment-route.yaml +73 -0
- provekit/templates/flows/sales-lead-severity-tiers.yaml +103 -0
- provekit/templates/flows/sales-lead-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/sales-lead-tag-enrich.yaml +43 -0
- provekit/templates/flows/sales-lead-tone-rewrite.yaml +43 -0
- provekit/templates/flows/sales-lead-translate-reply.yaml +58 -0
- provekit/templates/flows/sales-lead-triage-route.yaml +73 -0
- provekit/templates/flows/travel-action-items.yaml +43 -0
- provekit/templates/flows/travel-answer.yaml +43 -0
- provekit/templates/flows/travel-classify-intent.yaml +43 -0
- provekit/templates/flows/travel-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/travel-extract-to-json.yaml +43 -0
- provekit/templates/flows/travel-faq-match-answer.yaml +60 -0
- provekit/templates/flows/travel-keyword-extract.yaml +43 -0
- provekit/templates/flows/travel-language-route.yaml +103 -0
- provekit/templates/flows/travel-moderation-gate.yaml +73 -0
- provekit/templates/flows/travel-redact-summarize.yaml +59 -0
- provekit/templates/flows/travel-score-gate.yaml +73 -0
- provekit/templates/flows/travel-sentiment-route.yaml +73 -0
- provekit/templates/flows/travel-severity-tiers.yaml +103 -0
- provekit/templates/flows/travel-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/travel-tag-enrich.yaml +43 -0
- provekit/templates/flows/travel-tone-rewrite.yaml +43 -0
- provekit/templates/flows/travel-translate-reply.yaml +58 -0
- provekit/templates/flows/travel-triage-route.yaml +73 -0
- provekit-0.1.0.dist-info/METADATA +86 -0
- provekit-0.1.0.dist-info/RECORD +485 -0
- provekit-0.1.0.dist-info/WHEEL +5 -0
- provekit-0.1.0.dist-info/entry_points.txt +2 -0
- provekit-0.1.0.dist-info/top_level.txt +1 -0
provekit/routers/auth.py
ADDED
|
@@ -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))
|