secondshift 0.1.0b12__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 (371) hide show
  1. secondshift/__init__.py +1 -0
  2. secondshift/__main__.py +3 -0
  3. secondshift/cli.py +17 -0
  4. secondshift/config/__init__.py +0 -0
  5. secondshift/config/api.py +23 -0
  6. secondshift/config/app.py +62 -0
  7. secondshift/config/constants.py +279 -0
  8. secondshift/config/container_env.py +118 -0
  9. secondshift/config/first_run.py +40 -0
  10. secondshift/config/formatting.py +5 -0
  11. secondshift/config/gunicorn.py +31 -0
  12. secondshift/config/logging.py +40 -0
  13. secondshift/config/settings/__init__.py +0 -0
  14. secondshift/config/settings/base.py +524 -0
  15. secondshift/config/settings/dev.py +14 -0
  16. secondshift/config/settings/local.py +23 -0
  17. secondshift/config/settings/production.py +113 -0
  18. secondshift/config/settings/runner.py +140 -0
  19. secondshift/config/settings/test.py +47 -0
  20. secondshift/config/static_storage.py +45 -0
  21. secondshift/config/urls.py +42 -0
  22. secondshift/connections/__init__.py +0 -0
  23. secondshift/connections/access.py +27 -0
  24. secondshift/connections/admin.py +25 -0
  25. secondshift/connections/agents.py +224 -0
  26. secondshift/connections/api.py +747 -0
  27. secondshift/connections/apps.py +6 -0
  28. secondshift/connections/catalog.py +96 -0
  29. secondshift/connections/checks.py +22 -0
  30. secondshift/connections/dao.py +305 -0
  31. secondshift/connections/errors.py +13 -0
  32. secondshift/connections/execution.py +262 -0
  33. secondshift/connections/external.py +258 -0
  34. secondshift/connections/github.py +123 -0
  35. secondshift/connections/http_api/__init__.py +1 -0
  36. secondshift/connections/http_api/client.py +161 -0
  37. secondshift/connections/http_api/connections.py +187 -0
  38. secondshift/connections/http_api/spec.py +382 -0
  39. secondshift/connections/mcp/__init__.py +1 -0
  40. secondshift/connections/mcp/base.py +139 -0
  41. secondshift/connections/mcp/client.py +353 -0
  42. secondshift/connections/mcp/custom.py +86 -0
  43. secondshift/connections/mcp/github.py +256 -0
  44. secondshift/connections/mcp/oauth.py +352 -0
  45. secondshift/connections/mcp/providers.py +15 -0
  46. secondshift/connections/mcp/schemas.py +77 -0
  47. secondshift/connections/mcp/sentry.py +135 -0
  48. secondshift/connections/mcp/signin.py +206 -0
  49. secondshift/connections/migration_helpers.py +28 -0
  50. secondshift/connections/migrations/0001_initial.py +101 -0
  51. secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
  52. secondshift/connections/migrations/0003_require_approval.py +23 -0
  53. secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
  54. secondshift/connections/migrations/0005_agentconnection.py +36 -0
  55. secondshift/connections/migrations/0006_remove_is_default.py +23 -0
  56. secondshift/connections/migrations/0007_agent_last_check.py +18 -0
  57. secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
  58. secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
  59. secondshift/connections/migrations/0010_connection_attribution.py +18 -0
  60. secondshift/connections/migrations/__init__.py +0 -0
  61. secondshift/connections/model_providers.py +441 -0
  62. secondshift/connections/models.py +246 -0
  63. secondshift/connections/outbound.py +317 -0
  64. secondshift/connections/repository_ref.py +19 -0
  65. secondshift/connections/schemas.py +313 -0
  66. secondshift/connections/serialization.py +100 -0
  67. secondshift/connections/services.py +94 -0
  68. secondshift/connections/tools.py +38 -0
  69. secondshift/connections/webhooks.py +107 -0
  70. secondshift/core/__init__.py +0 -0
  71. secondshift/core/adapters.py +38 -0
  72. secondshift/core/admin.py +5 -0
  73. secondshift/core/api.py +104 -0
  74. secondshift/core/apps.py +20 -0
  75. secondshift/core/launcher.py +311 -0
  76. secondshift/core/local_agents.py +90 -0
  77. secondshift/core/local_output.py +120 -0
  78. secondshift/core/management/__init__.py +0 -0
  79. secondshift/core/management/commands/__init__.py +0 -0
  80. secondshift/core/management/commands/bootstrap_dev.py +33 -0
  81. secondshift/core/management/commands/dev_reset_db.py +204 -0
  82. secondshift/core/management/commands/login.py +23 -0
  83. secondshift/core/management/commands/logs.py +58 -0
  84. secondshift/core/management/commands/status.py +46 -0
  85. secondshift/core/management/commands/up.py +244 -0
  86. secondshift/core/middleware.py +21 -0
  87. secondshift/core/migrations/__init__.py +0 -0
  88. secondshift/core/schemas.py +71 -0
  89. secondshift/core/signin.py +58 -0
  90. secondshift/core/static/core/account.css +149 -0
  91. secondshift/core/static/core/admin.css +135 -0
  92. secondshift/core/templatetags/__init__.py +0 -0
  93. secondshift/core/templatetags/accounts.py +20 -0
  94. secondshift/core/terminal.py +27 -0
  95. secondshift/core/views.py +10 -0
  96. secondshift/gateway/__init__.py +0 -0
  97. secondshift/gateway/apps.py +6 -0
  98. secondshift/gateway/dao.py +137 -0
  99. secondshift/gateway/metering.py +171 -0
  100. secondshift/gateway/migrations/0001_initial.py +54 -0
  101. secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
  102. secondshift/gateway/migrations/__init__.py +0 -0
  103. secondshift/gateway/models.py +61 -0
  104. secondshift/gateway/pricing.py +39 -0
  105. secondshift/gateway/proxy.py +369 -0
  106. secondshift/gateway/tokens.py +68 -0
  107. secondshift/gateway/urls.py +17 -0
  108. secondshift/gateway/views.py +11 -0
  109. secondshift/github/__init__.py +6 -0
  110. secondshift/github/app.py +109 -0
  111. secondshift/github/authors.py +56 -0
  112. secondshift/github/credentials.py +59 -0
  113. secondshift/github/errors.py +2 -0
  114. secondshift/github/git.py +56 -0
  115. secondshift/github/identity.py +35 -0
  116. secondshift/github/installations.py +74 -0
  117. secondshift/github/issues.py +75 -0
  118. secondshift/github/permissions.py +49 -0
  119. secondshift/github/pulls.py +49 -0
  120. secondshift/github/repos.py +52 -0
  121. secondshift/github/rest.py +93 -0
  122. secondshift/github/ruff.toml +16 -0
  123. secondshift/github/tokens.py +52 -0
  124. secondshift/jobs/__init__.py +0 -0
  125. secondshift/jobs/access.py +12 -0
  126. secondshift/jobs/admin.py +83 -0
  127. secondshift/jobs/api.py +184 -0
  128. secondshift/jobs/apps.py +6 -0
  129. secondshift/jobs/automatic_grants.py +115 -0
  130. secondshift/jobs/autosync.py +78 -0
  131. secondshift/jobs/capabilities.py +43 -0
  132. secondshift/jobs/dao.py +258 -0
  133. secondshift/jobs/drafts.py +112 -0
  134. secondshift/jobs/grants.py +116 -0
  135. secondshift/jobs/limits.py +26 -0
  136. secondshift/jobs/management/__init__.py +0 -0
  137. secondshift/jobs/management/commands/__init__.py +0 -0
  138. secondshift/jobs/manifest.py +364 -0
  139. secondshift/jobs/migrations/0001_initial.py +87 -0
  140. secondshift/jobs/migrations/0002_job_history.py +18 -0
  141. secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
  142. secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
  143. secondshift/jobs/migrations/0005_job_triggers.py +18 -0
  144. secondshift/jobs/migrations/0006_job_webhook.py +18 -0
  145. secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
  146. secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
  147. secondshift/jobs/migrations/0009_job_subagents.py +18 -0
  148. secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
  149. secondshift/jobs/migrations/__init__.py +0 -0
  150. secondshift/jobs/models.py +199 -0
  151. secondshift/jobs/needs.py +154 -0
  152. secondshift/jobs/plan.py +176 -0
  153. secondshift/jobs/planning.py +386 -0
  154. secondshift/jobs/readiness.py +46 -0
  155. secondshift/jobs/repository_sync.py +199 -0
  156. secondshift/jobs/scheduling.py +26 -0
  157. secondshift/jobs/schemas.py +207 -0
  158. secondshift/jobs/serialization.py +218 -0
  159. secondshift/jobs/skill_needs.py +189 -0
  160. secondshift/jobs/sync.py +252 -0
  161. secondshift/jobs/tool_policy.py +82 -0
  162. secondshift/jobs/tool_rules.py +200 -0
  163. secondshift/mcp_server/__init__.py +6 -0
  164. secondshift/mcp_server/api.py +79 -0
  165. secondshift/mcp_server/apps.py +7 -0
  166. secondshift/mcp_server/audit.py +22 -0
  167. secondshift/mcp_server/core.py +12 -0
  168. secondshift/mcp_server/dao.py +23 -0
  169. secondshift/mcp_server/migrations/0001_initial.py +33 -0
  170. secondshift/mcp_server/migrations/__init__.py +0 -0
  171. secondshift/mcp_server/models.py +40 -0
  172. secondshift/mcp_server/schemas.py +204 -0
  173. secondshift/mcp_server/tools/__init__.py +9 -0
  174. secondshift/mcp_server/tools/jobs.py +106 -0
  175. secondshift/mcp_server/tools/manifests.py +127 -0
  176. secondshift/mcp_server/tools/repositories.py +42 -0
  177. secondshift/mcp_server/tools/runs.py +86 -0
  178. secondshift/mcp_server/tools/tasks.py +103 -0
  179. secondshift/mcp_server/tools_registry.py +81 -0
  180. secondshift/repositories/__init__.py +0 -0
  181. secondshift/repositories/access.py +13 -0
  182. secondshift/repositories/admin.py +19 -0
  183. secondshift/repositories/api.py +296 -0
  184. secondshift/repositories/apps.py +6 -0
  185. secondshift/repositories/dao.py +114 -0
  186. secondshift/repositories/external_skills.py +224 -0
  187. secondshift/repositories/github.py +27 -0
  188. secondshift/repositories/github_access.py +57 -0
  189. secondshift/repositories/github_check.py +279 -0
  190. secondshift/repositories/github_sync.py +181 -0
  191. secondshift/repositories/github_views.py +90 -0
  192. secondshift/repositories/local.py +198 -0
  193. secondshift/repositories/management/__init__.py +0 -0
  194. secondshift/repositories/management/commands/__init__.py +0 -0
  195. secondshift/repositories/management/commands/prune_source_cache.py +15 -0
  196. secondshift/repositories/migrations/0001_initial.py +45 -0
  197. secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
  198. secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
  199. secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
  200. secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
  201. secondshift/repositories/migrations/0006_allow_issues.py +18 -0
  202. secondshift/repositories/migrations/__init__.py +0 -0
  203. secondshift/repositories/mirrors.py +50 -0
  204. secondshift/repositories/models.py +104 -0
  205. secondshift/repositories/pull_requests.py +199 -0
  206. secondshift/repositories/schemas.py +102 -0
  207. secondshift/repositories/serialization.py +133 -0
  208. secondshift/repositories/skill_source.py +64 -0
  209. secondshift/repositories/snapshots.py +238 -0
  210. secondshift/repositories/sources.py +256 -0
  211. secondshift/repositories/status.py +67 -0
  212. secondshift/runner/README.md +65 -0
  213. secondshift/runner/__init__.py +1 -0
  214. secondshift/runner/agent_credentials.py +89 -0
  215. secondshift/runner/agent_image/Dockerfile +16 -0
  216. secondshift/runner/agent_image/agent.py +132 -0
  217. secondshift/runner/agent_image/claude.py +254 -0
  218. secondshift/runner/agent_image/codex.py +84 -0
  219. secondshift/runner/agent_image/outcomes.py +120 -0
  220. secondshift/runner/agent_image/subagent_hook.py +71 -0
  221. secondshift/runner/agent_protocol.py +41 -0
  222. secondshift/runner/agents.py +254 -0
  223. secondshift/runner/api.py +123 -0
  224. secondshift/runner/apps.py +6 -0
  225. secondshift/runner/client.py +168 -0
  226. secondshift/runner/collection.py +276 -0
  227. secondshift/runner/dao.py +64 -0
  228. secondshift/runner/dev.py +19 -0
  229. secondshift/runner/images.py +57 -0
  230. secondshift/runner/migrations/0001_initial.py +48 -0
  231. secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
  232. secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
  233. secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
  234. secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
  235. secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
  236. secondshift/runner/migrations/__init__.py +0 -0
  237. secondshift/runner/models.py +53 -0
  238. secondshift/runner/protocol.py +97 -0
  239. secondshift/runner/schemas.py +36 -0
  240. secondshift/runner/services.py +465 -0
  241. secondshift/runner/sources.py +130 -0
  242. secondshift/runner/urls.py +7 -0
  243. secondshift/runs/__init__.py +0 -0
  244. secondshift/runs/access.py +13 -0
  245. secondshift/runs/admin.py +89 -0
  246. secondshift/runs/api.py +211 -0
  247. secondshift/runs/apps.py +11 -0
  248. secondshift/runs/artifacts.py +261 -0
  249. secondshift/runs/dao.py +640 -0
  250. secondshift/runs/deliveries.py +80 -0
  251. secondshift/runs/execution/__init__.py +0 -0
  252. secondshift/runs/execution/access_needs.py +89 -0
  253. secondshift/runs/execution/agent.py +91 -0
  254. secondshift/runs/execution/agent_broker.py +198 -0
  255. secondshift/runs/execution/budgets.py +177 -0
  256. secondshift/runs/execution/cli_agent.py +167 -0
  257. secondshift/runs/execution/consent.py +58 -0
  258. secondshift/runs/execution/deadlines.py +47 -0
  259. secondshift/runs/execution/external_tools.py +84 -0
  260. secondshift/runs/execution/issues.py +99 -0
  261. secondshift/runs/execution/model_gateway.py +90 -0
  262. secondshift/runs/execution/payload.py +63 -0
  263. secondshift/runs/execution/result.py +59 -0
  264. secondshift/runs/execution/run.py +345 -0
  265. secondshift/runs/execution/subagents.py +246 -0
  266. secondshift/runs/execution/tool_broker.py +123 -0
  267. secondshift/runs/execution/workspace_tools.py +248 -0
  268. secondshift/runs/history.py +182 -0
  269. secondshift/runs/inputs.py +49 -0
  270. secondshift/runs/issue_delivery.py +144 -0
  271. secondshift/runs/management/__init__.py +0 -0
  272. secondshift/runs/management/commands/__init__.py +0 -0
  273. secondshift/runs/management/commands/prune_runs.py +12 -0
  274. secondshift/runs/management/commands/run_dbos_worker.py +129 -0
  275. secondshift/runs/migration_helpers.py +313 -0
  276. secondshift/runs/migrations/0001_initial.py +69 -0
  277. secondshift/runs/migrations/0002_run_started_by.py +18 -0
  278. secondshift/runs/migrations/0003_agentlease.py +25 -0
  279. secondshift/runs/migrations/0004_task_assignment.py +39 -0
  280. secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
  281. secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
  282. secondshift/runs/migrations/0007_run_delivery.py +35 -0
  283. secondshift/runs/migrations/0008_run_outputs.py +18 -0
  284. secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
  285. secondshift/runs/migrations/0010_run_issue.py +32 -0
  286. secondshift/runs/migrations/0011_issues_delivery.py +23 -0
  287. secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
  288. secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
  289. secondshift/runs/migrations/0014_run_access_need.py +30 -0
  290. secondshift/runs/migrations/__init__.py +0 -0
  291. secondshift/runs/models.py +340 -0
  292. secondshift/runs/orchestration/__init__.py +0 -0
  293. secondshift/runs/orchestration/admission.py +15 -0
  294. secondshift/runs/orchestration/dbos_runtime.py +43 -0
  295. secondshift/runs/orchestration/dispatch.py +130 -0
  296. secondshift/runs/orchestration/periodic.py +39 -0
  297. secondshift/runs/orchestration/reconciliation.py +98 -0
  298. secondshift/runs/orchestration/schedules.py +51 -0
  299. secondshift/runs/orchestration/workflows.py +480 -0
  300. secondshift/runs/outputs.py +52 -0
  301. secondshift/runs/publishing.py +162 -0
  302. secondshift/runs/queue.py +26 -0
  303. secondshift/runs/reporting/__init__.py +0 -0
  304. secondshift/runs/reporting/collection.py +26 -0
  305. secondshift/runs/reporting/connection_usage.py +45 -0
  306. secondshift/runs/reporting/failures.py +150 -0
  307. secondshift/runs/reporting/health.py +158 -0
  308. secondshift/runs/reporting/notifications.py +258 -0
  309. secondshift/runs/reporting/queries.py +60 -0
  310. secondshift/runs/retention.py +35 -0
  311. secondshift/runs/schemas.py +192 -0
  312. secondshift/runs/serialization.py +245 -0
  313. secondshift/runs/services.py +176 -0
  314. secondshift/runs/signals.py +25 -0
  315. secondshift/runs/submissions.py +236 -0
  316. secondshift/runs/transitions.py +84 -0
  317. secondshift/templates/account/password_reset_help_text.html +1 -0
  318. secondshift/templates/admin/base_site.html +24 -0
  319. secondshift/templates/allauth/elements/provider.html +9 -0
  320. secondshift/templates/allauth/layouts/base.html +67 -0
  321. secondshift/templates/socialaccount/snippets/login.html +9 -0
  322. secondshift/templates/workspaces/invite_unusable.html +10 -0
  323. secondshift/triggers/__init__.py +0 -0
  324. secondshift/triggers/admin.py +19 -0
  325. secondshift/triggers/api.py +116 -0
  326. secondshift/triggers/apps.py +6 -0
  327. secondshift/triggers/dao.py +66 -0
  328. secondshift/triggers/filters.py +48 -0
  329. secondshift/triggers/migrations/0001_initial.py +55 -0
  330. secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
  331. secondshift/triggers/migrations/__init__.py +0 -0
  332. secondshift/triggers/models.py +83 -0
  333. secondshift/triggers/schemas.py +58 -0
  334. secondshift/triggers/serialization.py +91 -0
  335. secondshift/triggers/services.py +246 -0
  336. secondshift/triggers/verification.py +84 -0
  337. secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
  338. secondshift/web/assets/index-D27gB9_L.css +1 -0
  339. secondshift/web/favicon.svg +19 -0
  340. secondshift/web/index.html +16 -0
  341. secondshift/web/secondshift-mark.svg +16 -0
  342. secondshift/workspaces/__init__.py +0 -0
  343. secondshift/workspaces/access.py +181 -0
  344. secondshift/workspaces/admin.py +37 -0
  345. secondshift/workspaces/api.py +264 -0
  346. secondshift/workspaces/apps.py +6 -0
  347. secondshift/workspaces/context_processors.py +11 -0
  348. secondshift/workspaces/dao.py +281 -0
  349. secondshift/workspaces/invites.py +150 -0
  350. secondshift/workspaces/limits.py +197 -0
  351. secondshift/workspaces/management/__init__.py +0 -0
  352. secondshift/workspaces/management/commands/__init__.py +0 -0
  353. secondshift/workspaces/management/commands/create_workspace.py +39 -0
  354. secondshift/workspaces/migrations/0001_initial.py +67 -0
  355. secondshift/workspaces/migrations/0002_api_token.py +34 -0
  356. secondshift/workspaces/migrations/0003_execution_default.py +29 -0
  357. secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
  358. secondshift/workspaces/migrations/__init__.py +0 -0
  359. secondshift/workspaces/models.py +169 -0
  360. secondshift/workspaces/owners.py +58 -0
  361. secondshift/workspaces/schemas.py +149 -0
  362. secondshift/workspaces/serialization.py +138 -0
  363. secondshift/workspaces/server_settings.py +140 -0
  364. secondshift/workspaces/services.py +94 -0
  365. secondshift/workspaces/tokens.py +108 -0
  366. secondshift/workspaces/views.py +32 -0
  367. secondshift-0.1.0b12.dist-info/METADATA +270 -0
  368. secondshift-0.1.0b12.dist-info/RECORD +371 -0
  369. secondshift-0.1.0b12.dist-info/WHEEL +4 -0
  370. secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
  371. secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,747 @@
1
+ from typing import Any
2
+ from urllib.parse import urlencode
3
+
4
+ from django.conf import settings
5
+ from django.core.exceptions import ValidationError
6
+ from django.http import HttpRequest, HttpResponseRedirect
7
+ from django.shortcuts import get_object_or_404
8
+ from ninja import Router, Status
9
+ from ninja.errors import HttpError
10
+ from ninja.security import django_auth
11
+
12
+ from secondshift.config.constants import SUMMARY_MAX_CHARS
13
+ from secondshift.connections.agents import (
14
+ AgentConnectionError,
15
+ check_agent_connection,
16
+ disconnect_agent_connection,
17
+ reconnect_agent_connection,
18
+ save_agent_connection,
19
+ )
20
+ from secondshift.connections.dao import (
21
+ github_token_sources,
22
+ own_agent_connections,
23
+ workspace_agent_connections,
24
+ workspace_connections,
25
+ )
26
+ from secondshift.connections.execution import (
27
+ apply_model_default,
28
+ execution_configuration,
29
+ save_execution_preference,
30
+ use_when_nothing_runs,
31
+ )
32
+ from secondshift.connections.github import (
33
+ connect_token_source,
34
+ is_github_source,
35
+ set_attribution,
36
+ )
37
+ from secondshift.connections.http_api.connections import (
38
+ create_api_connection,
39
+ recheck_api_connection,
40
+ summarize,
41
+ update_api_connection,
42
+ )
43
+ from secondshift.connections.http_api.spec import APIError
44
+ from secondshift.connections.mcp.base import MCPError
45
+ from secondshift.connections.mcp.client import (
46
+ create_mcp_connection,
47
+ recheck_mcp_connection,
48
+ update_mcp_connection,
49
+ )
50
+ from secondshift.connections.mcp.oauth import inspect_server, requested_scopes
51
+ from secondshift.connections.mcp.providers import MCP_PROVIDERS
52
+ from secondshift.connections.mcp.signin import (
53
+ SignInError,
54
+ complete_authorization,
55
+ start_authorization,
56
+ )
57
+ from secondshift.connections.model_providers import (
58
+ CheckFailed,
59
+ ModelProviderError,
60
+ check_model_connection,
61
+ checked_api_key,
62
+ create_model_connection,
63
+ fetch_models,
64
+ update_model_connection,
65
+ validated_endpoint,
66
+ )
67
+ from secondshift.connections.models import AgentConnection, Connection
68
+ from secondshift.connections.schemas import (
69
+ AgentConnectionInput,
70
+ AgentConnectionSchema,
71
+ AgentRunLimitInput,
72
+ APIConnectionCreateSchema,
73
+ APIConnectionUpdateSchema,
74
+ APISpecSchema,
75
+ APISpecSummarySchema,
76
+ AttributionSchema,
77
+ ConnectionSchema,
78
+ ConnectionTestSchema,
79
+ ExecutionConfigurationSchema,
80
+ ExecutionPreferenceSchema,
81
+ GitHubTokenConnectSchema,
82
+ MCPConnectionCreateSchema,
83
+ MCPConnectionUpdateSchema,
84
+ MCPInspectResultSchema,
85
+ MCPInspectSchema,
86
+ MCPProviderSchema,
87
+ MCPSettingSchema,
88
+ MCPSignInSchema,
89
+ MCPSignInStartSchema,
90
+ ModelConnectionCreateSchema,
91
+ ModelConnectionUpdateSchema,
92
+ WebhookConnectionCreateSchema,
93
+ WebhookConnectionUpdateSchema,
94
+ WebhookSampleSchema,
95
+ )
96
+ from secondshift.connections.serialization import (
97
+ serialize_connection,
98
+ with_recent_runs,
99
+ )
100
+ from secondshift.connections.services import (
101
+ ConnectionConflict,
102
+ create_webhook_connection,
103
+ revoke_connection,
104
+ update_webhook_connection,
105
+ )
106
+ from secondshift.connections.webhooks import (
107
+ WebhookError,
108
+ connection_test_payload,
109
+ deliver_webhook,
110
+ )
111
+ from secondshift.github.errors import GitHubError
112
+ from secondshift.jobs.automatic_grants import approve_waiting_for, set_require_approval
113
+ from secondshift.jobs.grants import delete_connection
114
+ from secondshift.repositories.github_check import check_source
115
+ from secondshift.runs.reporting.connection_usage import (
116
+ ConnectionUsage,
117
+ connection_usage,
118
+ )
119
+ from secondshift.runs.reporting.notifications import sample_run_report_payload
120
+ from secondshift.workspaces.access import (
121
+ Action,
122
+ current_workspace,
123
+ require,
124
+ )
125
+
126
+ router = Router(auth=django_auth, tags=["connections"])
127
+
128
+
129
+ @router.get("/connections", response=list[ConnectionSchema], auth=django_auth)
130
+ def connections(request: HttpRequest) -> list[ConnectionSchema]:
131
+ workspace = current_workspace(request)
132
+ if not workspace:
133
+ return []
134
+ usage = connection_usage(workspace)
135
+ return [
136
+ serialize_connection(item, usage.get(item.pk, ConnectionUsage()))
137
+ for item in workspace_connections(workspace.pk)
138
+ ]
139
+
140
+
141
+ @router.get(
142
+ "/connections/mcp-providers", response=list[MCPProviderSchema], auth=django_auth
143
+ )
144
+ def list_mcp_providers(request: HttpRequest) -> list[MCPProviderSchema]:
145
+ """The built-in MCP servers an owner can connect. Never includes secrets."""
146
+ return [
147
+ MCPProviderSchema(
148
+ key=provider.key,
149
+ label=provider.label,
150
+ description=provider.description,
151
+ default_binding=provider.default_binding,
152
+ token_label=provider.token_label,
153
+ token_hint=provider.token_hint,
154
+ setup_url=provider.setup_url,
155
+ setup_help=provider.setup_help,
156
+ settings=[
157
+ MCPSettingSchema(
158
+ key=item.key,
159
+ label=item.label,
160
+ pattern=item.pattern,
161
+ required=item.required,
162
+ help=item.help,
163
+ placeholder=item.placeholder,
164
+ )
165
+ for item in provider.settings
166
+ ],
167
+ tools=list(provider.tools),
168
+ auth_methods=list(provider.auth_methods),
169
+ owner_selects_tools=provider.owner_selects_tools,
170
+ )
171
+ for provider in MCP_PROVIDERS.values()
172
+ ]
173
+
174
+
175
+ @router.post("/connections/github-token", response=ConnectionSchema, auth=django_auth)
176
+ def connect_github_token(
177
+ request: HttpRequest, payload: GitHubTokenConnectSchema
178
+ ) -> ConnectionSchema:
179
+ """Reach repositories with a personal access token instead of the App."""
180
+ workspace = require(request, Action.ADD_GITHUB_REPOSITORIES)
181
+ replacing = None
182
+ if payload.connection_id is not None:
183
+ replacing = get_object_or_404(
184
+ github_token_sources(workspace.pk), pk=payload.connection_id
185
+ )
186
+ try:
187
+ connection = connect_token_source(workspace, payload.token, replacing)
188
+ except GitHubError as error:
189
+ raise HttpError(422, str(error)) from error
190
+ return serialize_connection(connection)
191
+
192
+
193
+ @router.post(
194
+ "/connections/{connection_id}/attribution",
195
+ response=ConnectionSchema,
196
+ auth=django_auth,
197
+ )
198
+ def update_attribution(
199
+ request: HttpRequest, connection_id: int, payload: AttributionSchema
200
+ ) -> ConnectionSchema:
201
+ """Whether pull requests, issues and commits through this source name SecondShift."""
202
+ connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
203
+ if not is_github_source(connection):
204
+ raise HttpError(404, "Not Found")
205
+ return serialize_connection(set_attribution(connection, payload.attribution))
206
+
207
+
208
+ @router.post("/connections/mcp", response=ConnectionSchema, auth=django_auth)
209
+ def create_mcp(
210
+ request: HttpRequest, payload: MCPConnectionCreateSchema
211
+ ) -> ConnectionSchema:
212
+ workspace = require(request, Action.ADD_MCP_CONNECTIONS)
213
+ try:
214
+ connection = create_mcp_connection(
215
+ workspace,
216
+ payload.provider,
217
+ payload.binding_key,
218
+ payload.display_name,
219
+ payload.token,
220
+ payload.settings,
221
+ payload.auth,
222
+ )
223
+ except ConnectionConflict as error:
224
+ raise HttpError(409, str(error)) from error
225
+ except MCPError as error:
226
+ raise HttpError(422, str(error)) from error
227
+ except ValidationError as error:
228
+ raise HttpError(422, "The MCP connection configuration is invalid.") from error
229
+ return apply_approval(connection, payload.require_approval)
230
+
231
+
232
+ @router.post(
233
+ "/connections/{connection_id}/mcp",
234
+ response=ConnectionSchema,
235
+ auth=django_auth,
236
+ )
237
+ def update_mcp(
238
+ request: HttpRequest, connection_id: int, payload: MCPConnectionUpdateSchema
239
+ ) -> ConnectionSchema:
240
+ connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
241
+ try:
242
+ connection = update_mcp_connection(
243
+ connection,
244
+ payload.display_name,
245
+ payload.token,
246
+ payload.settings,
247
+ payload.tools,
248
+ )
249
+ except ConnectionConflict as error:
250
+ raise HttpError(409, str(error)) from error
251
+ except MCPError as error:
252
+ raise HttpError(422, str(error)) from error
253
+ except ValidationError as error:
254
+ raise HttpError(422, "The MCP connection configuration is invalid.") from error
255
+ return apply_approval(connection, payload.require_approval)
256
+
257
+
258
+ @router.post(
259
+ "/connections/api/inspect", response=APISpecSummarySchema, auth=django_auth
260
+ )
261
+ def inspect_api_spec(
262
+ request: HttpRequest, payload: APISpecSchema
263
+ ) -> APISpecSummarySchema:
264
+ """The spec's title, servers, and operation counts, before connecting."""
265
+ require(request, Action.ADD_API_CONNECTIONS)
266
+ try:
267
+ summary = summarize(payload.spec_url.strip(), payload.spec_text)
268
+ except APIError as error:
269
+ raise HttpError(422, str(error)) from error
270
+ return APISpecSummarySchema(
271
+ title=summary.title,
272
+ servers=list(summary.servers),
273
+ operations=summary.operations,
274
+ read_only=summary.read_only,
275
+ )
276
+
277
+
278
+ @router.post("/connections/api", response=ConnectionSchema, auth=django_auth)
279
+ def create_api(
280
+ request: HttpRequest, payload: APIConnectionCreateSchema
281
+ ) -> ConnectionSchema:
282
+ workspace = require(request, Action.ADD_API_CONNECTIONS)
283
+ try:
284
+ connection = create_api_connection(
285
+ workspace,
286
+ payload.binding_key,
287
+ payload.display_name,
288
+ payload.spec_url.strip(),
289
+ payload.spec_text,
290
+ payload.base_url,
291
+ payload.auth,
292
+ payload.header.strip(),
293
+ payload.token,
294
+ )
295
+ except ConnectionConflict as error:
296
+ raise HttpError(409, str(error)) from error
297
+ except APIError as error:
298
+ raise HttpError(422, str(error)) from error
299
+ except ValidationError as error:
300
+ raise HttpError(422, "The API connection configuration is invalid.") from error
301
+ return apply_approval(connection, payload.require_approval)
302
+
303
+
304
+ @router.post(
305
+ "/connections/{connection_id}/api", response=ConnectionSchema, auth=django_auth
306
+ )
307
+ def update_api(
308
+ request: HttpRequest, connection_id: int, payload: APIConnectionUpdateSchema
309
+ ) -> ConnectionSchema:
310
+ connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
311
+ try:
312
+ connection = update_api_connection(
313
+ connection,
314
+ payload.display_name,
315
+ payload.token,
316
+ payload.base_url,
317
+ payload.operations,
318
+ )
319
+ except ConnectionConflict as error:
320
+ raise HttpError(409, str(error)) from error
321
+ except APIError as error:
322
+ raise HttpError(422, str(error)) from error
323
+ except ValidationError as error:
324
+ raise HttpError(422, "The API connection configuration is invalid.") from error
325
+ return apply_approval(connection, payload.require_approval)
326
+
327
+
328
+ @router.post(
329
+ "/connections/mcp/inspect", response=MCPInspectResultSchema, auth=django_auth
330
+ )
331
+ def inspect_mcp(
332
+ request: HttpRequest, payload: MCPInspectSchema
333
+ ) -> MCPInspectResultSchema:
334
+ """Whether a server offers sign-in, and which scopes it lists."""
335
+ require(request, Action.ADD_MCP_CONNECTIONS)
336
+ try:
337
+ server = inspect_server(payload.provider, payload.settings)
338
+ except MCPError as error:
339
+ raise HttpError(422, str(error)) from error
340
+ if server is None:
341
+ return MCPInspectResultSchema(oauth=False, scopes=[])
342
+ return MCPInspectResultSchema(oauth=True, scopes=list(server.scopes_supported))
343
+
344
+
345
+ @router.post(
346
+ "/connections/mcp/oauth/start", response=MCPSignInStartSchema, auth=django_auth
347
+ )
348
+ def start_mcp_sign_in(
349
+ request: HttpRequest, payload: MCPSignInSchema
350
+ ) -> MCPSignInStartSchema:
351
+ workspace = require(request, Action.ADD_MCP_CONNECTIONS)
352
+ connection = (
353
+ get_object_or_404(
354
+ workspace_connections(workspace.pk, Connection.Kind.MCP),
355
+ pk=payload.connection_id,
356
+ )
357
+ if payload.connection_id
358
+ else None
359
+ )
360
+ if connection is None and not payload.binding_key:
361
+ raise HttpError(422, "Enter a binding.")
362
+ try:
363
+ url = start_authorization(
364
+ workspace,
365
+ request.user,
366
+ connection.provider if connection else payload.provider,
367
+ payload.binding_key,
368
+ payload.display_name,
369
+ connection.settings if connection else payload.settings,
370
+ requested_scopes(connection) if connection else payload.scopes,
371
+ connection,
372
+ payload.require_approval,
373
+ )
374
+ except ConnectionConflict as error:
375
+ raise HttpError(409, str(error)) from error
376
+ except MCPError as error:
377
+ raise HttpError(422, str(error)) from error
378
+ return MCPSignInStartSchema(authorize_url=url)
379
+
380
+
381
+ @router.get("/connections/oauth/callback", auth=None, include_in_schema=False)
382
+ def finish_mcp_sign_in(request: HttpRequest) -> HttpResponseRedirect:
383
+ """The browser lands here from the sign-in page. Always redirect back."""
384
+ back = f"{settings.FRONTEND_URL.rstrip('/')}/connections"
385
+ if not request.user.is_authenticated:
386
+ return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'invalid'})}")
387
+ try:
388
+ connection = complete_authorization(
389
+ request.user,
390
+ request.GET.get("state", "")[:200],
391
+ request.GET.get("code", "")[:2000],
392
+ request.GET.get("error", "")[:200],
393
+ )
394
+ # Only fixed codes go in the URL, so a crafted link cannot show any text.
395
+ except SignInError as error:
396
+ return HttpResponseRedirect(f"{back}?{urlencode({'oauth': error.code})}")
397
+ except ConnectionConflict:
398
+ return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'taken'})}")
399
+ except (MCPError, ValidationError):
400
+ return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'failed'})}")
401
+ approve_waiting_for(connection)
402
+ query = {"oauth": "connected", "capability": connection.binding_key}
403
+ return HttpResponseRedirect(f"{back}?{urlencode(query)}")
404
+
405
+
406
+ @router.post("/connections/webhook", response=ConnectionSchema, auth=django_auth)
407
+ def create_webhook(
408
+ request: HttpRequest,
409
+ payload: WebhookConnectionCreateSchema,
410
+ ) -> ConnectionSchema:
411
+ workspace = require(request, Action.ADD_CONNECTIONS)
412
+ try:
413
+ connection = create_webhook_connection(
414
+ workspace,
415
+ payload.binding_key,
416
+ payload.display_name,
417
+ payload.url,
418
+ )
419
+ except ConnectionConflict as error:
420
+ raise HttpError(409, str(error)) from error
421
+ except WebhookError as error:
422
+ raise HttpError(422, str(error)) from error
423
+ except ValidationError as error:
424
+ raise HttpError(422, " ".join(error.messages)) from error
425
+ return apply_approval(connection, payload.require_approval)
426
+
427
+
428
+ def apply_approval(
429
+ connection: Connection, require_approval: bool | None
430
+ ) -> ConnectionSchema:
431
+ """Save the owner's approval choice and approve requests it now allows."""
432
+ if require_approval is None:
433
+ approve_waiting_for(connection)
434
+ else:
435
+ set_require_approval(connection, require_approval)
436
+ return serialize_connection(connection)
437
+
438
+
439
+ def owned_connection(
440
+ request: HttpRequest, connection_id: int, action: Action
441
+ ) -> Connection:
442
+ workspace = require(request, action)
443
+ return get_object_or_404(workspace_connections(workspace.pk), pk=connection_id)
444
+
445
+
446
+ @router.post(
447
+ "/connections/{connection_id}/webhook",
448
+ response=ConnectionSchema,
449
+ auth=django_auth,
450
+ )
451
+ def update_webhook(
452
+ request: HttpRequest,
453
+ connection_id: int,
454
+ payload: WebhookConnectionUpdateSchema,
455
+ ) -> ConnectionSchema:
456
+ connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
457
+ try:
458
+ connection = update_webhook_connection(
459
+ connection, payload.display_name, payload.url
460
+ )
461
+ except ConnectionConflict as error:
462
+ raise HttpError(409, str(error)) from error
463
+ except WebhookError as error:
464
+ raise HttpError(422, str(error)) from error
465
+ except ValidationError as error:
466
+ raise HttpError(422, " ".join(error.messages)) from error
467
+ return apply_approval(connection, payload.require_approval)
468
+
469
+
470
+ @router.post("/connections/model/models", response=list[str], auth=django_auth)
471
+ def discover_model_provider_models(
472
+ request: HttpRequest, payload: ModelConnectionCreateSchema
473
+ ) -> list[str]:
474
+ require(request, Action.ADD_MODEL_PROVIDERS)
475
+ try:
476
+ connection = Connection(
477
+ provider=payload.provider,
478
+ endpoint_url=validated_endpoint(payload.provider, payload.base_url),
479
+ access_token=checked_api_key(payload.provider, payload.api_key),
480
+ )
481
+ return fetch_models(connection)
482
+ except (ModelProviderError, CheckFailed) as error:
483
+ raise HttpError(422, str(error)) from error
484
+
485
+
486
+ @router.post(
487
+ "/connections/{connection_id}/model/models", response=list[str], auth=django_auth
488
+ )
489
+ def refresh_model_provider_models(
490
+ request: HttpRequest, connection_id: int, payload: ModelConnectionUpdateSchema
491
+ ) -> list[str]:
492
+ connection = owned_connection(request, connection_id, Action.EDIT_MODEL_PROVIDERS)
493
+ if connection.kind != Connection.Kind.MODEL:
494
+ raise HttpError(422, "This is not a model provider connection.")
495
+ try:
496
+ # Preview changes without saving credentials, settings, or discovery results.
497
+ preview = Connection(
498
+ provider=connection.provider,
499
+ endpoint_url=validated_endpoint(
500
+ connection.provider,
501
+ payload.base_url
502
+ if payload.base_url is not None
503
+ else connection.endpoint_url,
504
+ ),
505
+ access_token=payload.api_key.strip()
506
+ if payload.api_key and payload.api_key.strip()
507
+ else connection.access_token,
508
+ )
509
+ return fetch_models(preview)
510
+ except (ModelProviderError, CheckFailed) as error:
511
+ raise HttpError(422, str(error)) from error
512
+
513
+
514
+ @router.post("/connections/model", response=ConnectionSchema, auth=django_auth)
515
+ def create_model_provider(
516
+ request: HttpRequest,
517
+ payload: ModelConnectionCreateSchema,
518
+ ) -> ConnectionSchema:
519
+ workspace = require(request, Action.ADD_MODEL_PROVIDERS)
520
+ try:
521
+ connection = create_model_connection(
522
+ workspace,
523
+ payload.provider,
524
+ payload.display_name,
525
+ payload.api_key,
526
+ endpoint_url=payload.base_url,
527
+ allowed_models=payload.allowed_models,
528
+ default_model=payload.default_model,
529
+ )
530
+ except ModelProviderError as error:
531
+ raise HttpError(422, str(error)) from error
532
+ except ValidationError as error:
533
+ raise HttpError(422, " ".join(error.messages)) from error
534
+ apply_model_default(connection, payload.is_default)
535
+ return serialize_connection(connection)
536
+
537
+
538
+ @router.post(
539
+ "/connections/{connection_id}/model",
540
+ response=ConnectionSchema,
541
+ auth=django_auth,
542
+ )
543
+ def update_model_provider(
544
+ request: HttpRequest,
545
+ connection_id: int,
546
+ payload: ModelConnectionUpdateSchema,
547
+ ) -> ConnectionSchema:
548
+ connection = owned_connection(request, connection_id, Action.EDIT_MODEL_PROVIDERS)
549
+ try:
550
+ connection = update_model_connection(
551
+ connection,
552
+ display_name=payload.display_name,
553
+ api_key=payload.api_key,
554
+ endpoint_url=payload.base_url,
555
+ allowed_models=payload.allowed_models,
556
+ default_model=payload.default_model,
557
+ )
558
+ except ModelProviderError as error:
559
+ raise HttpError(422, str(error)) from error
560
+ except ValidationError as error:
561
+ raise HttpError(422, " ".join(error.messages)) from error
562
+ apply_model_default(connection, payload.is_default)
563
+ return serialize_connection(connection)
564
+
565
+
566
+ @router.post(
567
+ "/connections/{connection_id}/revoke",
568
+ response=ConnectionSchema,
569
+ auth=django_auth,
570
+ )
571
+ def revoke(request: HttpRequest, connection_id: int) -> ConnectionSchema:
572
+ connection = owned_connection(request, connection_id, Action.DISCONNECT_CONNECTIONS)
573
+ return serialize_connection(revoke_connection(connection))
574
+
575
+
576
+ @router.delete(
577
+ "/connections/{connection_id}",
578
+ response={204: None},
579
+ auth=django_auth,
580
+ )
581
+ def remove_connection(request: HttpRequest, connection_id: int) -> Status[None]:
582
+ connection = owned_connection(request, connection_id, Action.DELETE_CONNECTIONS)
583
+ try:
584
+ delete_connection(connection)
585
+ except ConnectionConflict as error:
586
+ raise HttpError(409, str(error)) from error
587
+ return Status(204, None)
588
+
589
+
590
+ @router.post(
591
+ "/connections/{connection_id}/test",
592
+ response=ConnectionTestSchema,
593
+ auth=django_auth,
594
+ )
595
+ def test_connection(request: HttpRequest, connection_id: int) -> ConnectionTestSchema:
596
+ connection = owned_connection(request, connection_id, Action.TEST_CONNECTIONS)
597
+ if connection.kind == Connection.Kind.MCP:
598
+ try:
599
+ message = recheck_mcp_connection(connection)
600
+ except MCPError as error:
601
+ return ConnectionTestSchema(ok=False, status_code=None, message=str(error))
602
+ return ConnectionTestSchema(ok=True, status_code=None, message=message)
603
+ if connection.kind == Connection.Kind.API:
604
+ try:
605
+ message = recheck_api_connection(connection)
606
+ except APIError as error:
607
+ return ConnectionTestSchema(ok=False, status_code=None, message=str(error))
608
+ return ConnectionTestSchema(ok=True, status_code=None, message=message)
609
+ if connection.kind == Connection.Kind.MODEL:
610
+ check = check_model_connection(connection)
611
+ return ConnectionTestSchema(
612
+ ok=check.ok, status_code=None, message=check.message, models=check.models
613
+ )
614
+ if (
615
+ connection.kind == Connection.Kind.SOURCE
616
+ and connection.provider == Connection.Provider.GITHUB
617
+ ):
618
+ check = check_source(connection)
619
+ return ConnectionTestSchema(
620
+ ok=check.ok, status_code=None, message=check.message
621
+ )
622
+ if connection.provider != Connection.Provider.WEBHOOK:
623
+ raise HttpError(400, "Testing is not available for this connection yet.")
624
+ delivery = deliver_webhook(connection, connection_test_payload(connection))
625
+ return ConnectionTestSchema(
626
+ ok=delivery.ok,
627
+ status_code=delivery.status_code,
628
+ message=delivery.message,
629
+ )
630
+
631
+
632
+ @router.get(
633
+ "/connections/webhook/sample", response=WebhookSampleSchema, auth=django_auth
634
+ )
635
+ def webhook_sample(request: HttpRequest) -> WebhookSampleSchema:
636
+ """What a webhook connection will receive after each run."""
637
+ return WebhookSampleSchema(
638
+ payload=sample_run_report_payload(), summary_max_chars=SUMMARY_MAX_CHARS
639
+ )
640
+
641
+
642
+ @router.get("/agent-connections", response=list[AgentConnectionSchema])
643
+ def agent_connections(request: HttpRequest) -> list[AgentConnection]:
644
+ workspace = current_workspace(request)
645
+ if not workspace:
646
+ return []
647
+ return with_recent_runs(workspace_agent_connections(workspace), workspace.pk)
648
+
649
+
650
+ @router.post("/agent-connections", response=AgentConnectionSchema)
651
+ def connect_agent(
652
+ request: HttpRequest, payload: AgentConnectionInput
653
+ ) -> AgentConnection:
654
+ workspace = require(request, Action.ADD_CONNECTIONS)
655
+ try:
656
+ connection = save_agent_connection(
657
+ workspace,
658
+ request.user,
659
+ payload.backend,
660
+ payload.display_name,
661
+ payload.auth.get_secret_value(),
662
+ )
663
+ except AgentConnectionError as error:
664
+ raise HttpError(422, str(error)) from None
665
+ use_when_nothing_runs(workspace, login=connection)
666
+ return connection
667
+
668
+
669
+ @router.post("/agent-connections/{connection_id}", response=AgentConnectionSchema)
670
+ def reconnect_agent(
671
+ request: HttpRequest, connection_id: int, payload: AgentConnectionInput
672
+ ) -> AgentConnection:
673
+ workspace = require(request, Action.EDIT_CONNECTIONS)
674
+ connection = get_object_or_404(
675
+ own_agent_connections(workspace, request.user), pk=connection_id
676
+ )
677
+ try:
678
+ return reconnect_agent_connection(
679
+ connection,
680
+ request.user,
681
+ payload.backend,
682
+ payload.display_name,
683
+ payload.auth.get_secret_value(),
684
+ )
685
+ except AgentConnectionError as error:
686
+ raise HttpError(422, str(error)) from None
687
+
688
+
689
+ @router.post("/agent-connections/{connection_id}/check", response=AgentConnectionSchema)
690
+ def check_agent(request: HttpRequest, connection_id: int) -> AgentConnection:
691
+ """Only the login's owner checks it. The check uses their plan's allowance."""
692
+ workspace = require(request, Action.TEST_CONNECTIONS)
693
+ connection = get_object_or_404(
694
+ own_agent_connections(workspace, request.user), pk=connection_id
695
+ )
696
+ try:
697
+ check_agent_connection(connection)
698
+ except AgentConnectionError as error:
699
+ raise HttpError(409, str(error)) from None
700
+ return connection
701
+
702
+
703
+ @router.post(
704
+ "/agent-connections/{connection_id}/run-limit", response=AgentConnectionSchema
705
+ )
706
+ def set_agent_run_limit(
707
+ request: HttpRequest, connection_id: int, payload: AgentRunLimitInput
708
+ ) -> AgentConnection:
709
+ """Only the login's owner sets its cap. It is their subscription."""
710
+ workspace = require(request, Action.EDIT_CONNECTIONS)
711
+ connection = get_object_or_404(
712
+ own_agent_connections(workspace, request.user), pk=connection_id
713
+ )
714
+ connection.max_runs_per_day = payload.max_runs_per_day
715
+ connection.save(update_fields=["max_runs_per_day", "updated_at"])
716
+ return connection
717
+
718
+
719
+ @router.delete("/agent-connections/{connection_id}", response={204: None})
720
+ def disconnect_agent(request: HttpRequest, connection_id: int) -> Status[None]:
721
+ workspace = require(request, Action.DISCONNECT_CONNECTIONS)
722
+ connection = get_object_or_404(
723
+ own_agent_connections(workspace, request.user), pk=connection_id
724
+ )
725
+ # A cached copy the runner keeps is removed by the worker.
726
+ disconnect_agent_connection(connection)
727
+ return Status(204, None)
728
+
729
+
730
+ @router.get("/settings/agent", response=ExecutionConfigurationSchema)
731
+ def default_agent(request: HttpRequest) -> dict[str, Any]:
732
+ workspace = current_workspace(request)
733
+ if workspace is None:
734
+ raise HttpError(404, "Choose a workspace first.")
735
+ return execution_configuration(workspace, request.user)
736
+
737
+
738
+ @router.post("/settings/agent", response=ExecutionConfigurationSchema)
739
+ def set_default_agent(
740
+ request: HttpRequest, payload: ExecutionPreferenceSchema
741
+ ) -> dict[str, Any]:
742
+ workspace = require(request, Action.REVIEW_CAPABILITIES)
743
+ try:
744
+ save_execution_preference(workspace, payload.key, request.user)
745
+ except ValidationError as error:
746
+ raise HttpError(422, error.messages[0]) from None
747
+ return execution_configuration(workspace, request.user)