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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ConnectionsConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "secondshift.connections"
@@ -0,0 +1,96 @@
1
+ """What a manifest can ask for in a workspace: bindings and their tools.
2
+
3
+ Only names, descriptions, input schemas and whether a tool may change data
4
+ are listed. Endpoints, paths, auth details and credentials never are. Tools
5
+ the owner has not allowed are counted but not named.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from secondshift.connections.dao import catalog_connections
12
+ from secondshift.connections.external import changing_tools
13
+ from secondshift.connections.mcp.github import GITHUB_MCP
14
+ from secondshift.connections.mcp.sentry import SENTRY_MCP
15
+ from secondshift.connections.models import Connection
16
+ from secondshift.workspaces.models import Workspace
17
+
18
+ # Descriptions come from third-party servers. Long ones are cut.
19
+ DESCRIPTION_MAX = 500
20
+ BINDING_KINDS = (Connection.Kind.MCP, Connection.Kind.API, Connection.Kind.NOTIFICATION)
21
+ BUILT_IN_PROVIDERS = (GITHUB_MCP, SENTRY_MCP)
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CatalogTool:
26
+ name: str
27
+ description: str
28
+ input_schema: dict[str, Any]
29
+ may_change_data: bool
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CatalogBinding:
34
+ binding: str
35
+ kind: str
36
+ provider: str
37
+ connected: bool
38
+ tools: list[CatalogTool] = field(default_factory=list)
39
+ # Tools the service offers that the owner has not allowed.
40
+ hidden_tools: int = 0
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class BuiltInProvider:
45
+ provider: str
46
+ kind: str
47
+ tools: list[str]
48
+ # Bindings in this workspace that already connect it.
49
+ bindings: list[str]
50
+
51
+
52
+ def catalog_tool(name: str, definition: dict[str, Any], changing: bool) -> CatalogTool:
53
+ return CatalogTool(
54
+ name=name,
55
+ description=str(definition.get("description", ""))[:DESCRIPTION_MAX],
56
+ input_schema=dict(definition.get("inputSchema") or {}),
57
+ may_change_data=changing,
58
+ )
59
+
60
+
61
+ def catalog_binding(connection: Connection) -> CatalogBinding:
62
+ definitions = connection.tool_definitions
63
+ offered = connection.offered_tools
64
+ allowed = sorted(connection.available_tools)
65
+ changing = set(changing_tools(connection, allowed))
66
+ return CatalogBinding(
67
+ binding=connection.binding_key,
68
+ kind=connection.kind,
69
+ provider=connection.get_provider_display(),
70
+ connected=connection.is_connected,
71
+ tools=[
72
+ catalog_tool(name, definitions.get(name) or {}, name in changing)
73
+ for name in allowed
74
+ ],
75
+ hidden_tools=len(set(offered) - set(allowed)),
76
+ )
77
+
78
+
79
+ def workspace_catalog(
80
+ workspace: Workspace,
81
+ ) -> tuple[list[CatalogBinding], list[BuiltInProvider]]:
82
+ connections = catalog_connections(workspace, BINDING_KINDS)
83
+ built_ins = [
84
+ BuiltInProvider(
85
+ provider=provider.label,
86
+ kind=Connection.Kind.MCP,
87
+ tools=list(provider.tools),
88
+ bindings=[
89
+ connection.binding_key
90
+ for connection in connections
91
+ if connection.provider == provider.key
92
+ ],
93
+ )
94
+ for provider in BUILT_IN_PROVIDERS
95
+ ]
96
+ return [catalog_binding(connection) for connection in connections], built_ins
@@ -0,0 +1,22 @@
1
+ from django.utils import timezone
2
+
3
+ from secondshift.connections.models import Connection
4
+
5
+
6
+ def record_check(
7
+ connection: Connection, ok: bool, message: str, **details: object
8
+ ) -> None:
9
+ """Keep the latest check or delivery result, shown on the Connections page.
10
+
11
+ details add to it, such as a source's per-feature access. Never secrets.
12
+ """
13
+ connection.metadata = {
14
+ **connection.metadata,
15
+ "last_check": {
16
+ "ok": ok,
17
+ "message": message,
18
+ "checked_at": timezone.now().isoformat(),
19
+ **details,
20
+ },
21
+ }
22
+ connection.save(update_fields=["metadata", "updated_at"])
@@ -0,0 +1,305 @@
1
+ """Queries and writes on connections, sign-ins and agent logins."""
2
+
3
+ from collections import Counter
4
+ from collections.abc import Collection
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from django.contrib.auth.models import AbstractBaseUser
9
+ from django.db.models import Count, Exists, F, OuterRef, Q, QuerySet
10
+ from django.utils import timezone
11
+
12
+ from secondshift.connections.models import (
13
+ AgentConnection,
14
+ AgentCredentialCleanup,
15
+ Connection,
16
+ OAuthAuthorization,
17
+ )
18
+ from secondshift.workspaces.models import Workspace
19
+
20
+
21
+ def bound_connections(
22
+ workspace_id: int, bindings: Collection[str] | None = None
23
+ ) -> QuerySet[Connection]:
24
+ """The workspace's connections that have a binding key.
25
+
26
+ Pass `bindings` to keep only connections with those keys.
27
+ """
28
+ connections = Connection.objects.filter(workspace_id=workspace_id).exclude(
29
+ binding_key=""
30
+ )
31
+ if bindings is not None:
32
+ connections = connections.filter(binding_key__in=bindings)
33
+ return connections
34
+
35
+
36
+ def connections_by_binding(
37
+ workspace_id: int, bindings: Collection[str] | None = None
38
+ ) -> dict[str, Connection]:
39
+ """The workspace's bound connections, keyed by binding key."""
40
+ return {
41
+ connection.binding_key: connection
42
+ for connection in bound_connections(workspace_id, bindings)
43
+ }
44
+
45
+
46
+ def connection_for_binding(workspace_id: int, binding: str) -> Connection | None:
47
+ """The workspace's connection with this binding key, if there is one."""
48
+ return bound_connections(workspace_id, [binding]).first()
49
+
50
+
51
+ def auto_approved_connections(
52
+ workspace_ids: Collection[int], bindings: Collection[str]
53
+ ) -> QuerySet[Connection]:
54
+ """Connections with these keys that grant access without an owner's review."""
55
+ return Connection.objects.filter(
56
+ workspace_id__in=workspace_ids,
57
+ binding_key__in=bindings,
58
+ require_approval=False,
59
+ ).exclude(binding_key="")
60
+
61
+
62
+ def create_connection(**fields: Any) -> Connection:
63
+ return Connection.objects.create(**fields)
64
+
65
+
66
+ def get_connection(connection_id: int) -> Connection:
67
+ """The connection. Raises Connection.DoesNotExist when there is none."""
68
+ return Connection.objects.get(pk=connection_id)
69
+
70
+
71
+ def lock_connection(connection_id: int) -> Connection:
72
+ """Lock the connection for this transaction."""
73
+ return Connection.objects.select_for_update().get(pk=connection_id)
74
+
75
+
76
+ def connection_binding(connection_id: int) -> str | None:
77
+ """The connection's binding key, or None when it was deleted."""
78
+ return (
79
+ Connection.objects.filter(pk=connection_id)
80
+ .values_list("binding_key", flat=True)
81
+ .first()
82
+ )
83
+
84
+
85
+ def workspace_connections(
86
+ workspace_id: int, kind: str | None = None
87
+ ) -> QuerySet[Connection]:
88
+ connections = Connection.objects.filter(workspace_id=workspace_id)
89
+ return connections if kind is None else connections.filter(kind=kind)
90
+
91
+
92
+ def github_token_sources(workspace_id: int) -> QuerySet[Connection]:
93
+ """The workspace's GitHub sources that use a personal access token."""
94
+ return Connection.objects.filter(
95
+ workspace_id=workspace_id,
96
+ provider=Connection.Provider.GITHUB,
97
+ kind=Connection.Kind.SOURCE,
98
+ auth_strategy=Connection.AuthStrategy.API_KEY,
99
+ )
100
+
101
+
102
+ def connected_count(workspace_id: int) -> int:
103
+ return Connection.objects.filter(
104
+ workspace_id=workspace_id, status=Connection.Status.CONNECTED
105
+ ).count()
106
+
107
+
108
+ def binding_taken(workspace_id: int, binding_key: str) -> bool:
109
+ """Whether a connection in the workspace already uses this binding key."""
110
+ return Connection.objects.filter(
111
+ workspace_id=workspace_id, binding_key=binding_key
112
+ ).exists()
113
+
114
+
115
+ def catalog_connections(
116
+ workspace: Workspace, kinds: Collection[str]
117
+ ) -> list[Connection]:
118
+ """The workspace's bound connections of these kinds, by binding key."""
119
+ return list(
120
+ Connection.objects.filter(workspace=workspace, kind__in=kinds)
121
+ .exclude(binding_key="")
122
+ .order_by("binding_key")
123
+ )
124
+
125
+
126
+ def connected_github_sources(workspace_id: int | None = None) -> QuerySet[Connection]:
127
+ """Connected GitHub sources, in one workspace or in all of them."""
128
+ sources = Connection.objects.filter(
129
+ provider=Connection.Provider.GITHUB,
130
+ kind=Connection.Kind.SOURCE,
131
+ status=Connection.Status.CONNECTED,
132
+ )
133
+ if workspace_id is not None:
134
+ sources = sources.filter(workspace_id=workspace_id)
135
+ return sources
136
+
137
+
138
+ def has_github_token_sources() -> bool:
139
+ """Whether any connected GitHub source signs in with a personal token."""
140
+ return (
141
+ connected_github_sources()
142
+ .filter(auth_strategy=Connection.AuthStrategy.API_KEY)
143
+ .exists()
144
+ )
145
+
146
+
147
+ def save_github_source(
148
+ workspace: Workspace, external_account_id: str, fields: dict[str, Any]
149
+ ) -> Connection:
150
+ """Create or update the workspace's GitHub source for this account."""
151
+ connection, _ = Connection.objects.update_or_create(
152
+ workspace=workspace,
153
+ provider=Connection.Provider.GITHUB,
154
+ external_account_id=external_account_id,
155
+ defaults=fields,
156
+ )
157
+ return connection
158
+
159
+
160
+ def mark_expired(connection_id: int) -> None:
161
+ Connection.objects.filter(pk=connection_id).update(
162
+ status=Connection.Status.EXPIRED, updated_at=timezone.now()
163
+ )
164
+
165
+
166
+ def usable_models(workspace: Workspace) -> QuerySet[Connection]:
167
+ """The workspace's model providers that were not revoked."""
168
+ return Connection.objects.filter(
169
+ workspace=workspace, kind=Connection.Kind.MODEL
170
+ ).exclude(status=Connection.Status.REVOKED)
171
+
172
+
173
+ def connected_model(workspace_id: int, connection_id: int) -> Connection | None:
174
+ """The workspace's model provider while it is connected."""
175
+ return Connection.objects.filter(
176
+ pk=connection_id,
177
+ workspace_id=workspace_id,
178
+ kind=Connection.Kind.MODEL,
179
+ status=Connection.Status.CONNECTED,
180
+ ).first()
181
+
182
+
183
+ def has_models_or_logins(workspace_id: int) -> bool:
184
+ """Whether a model provider or an agent login is saved. One query for both."""
185
+ return Workspace.objects.filter(
186
+ Exists(
187
+ Connection.objects.filter(
188
+ workspace=OuterRef("pk"), kind=Connection.Kind.MODEL
189
+ )
190
+ )
191
+ | Exists(AgentConnection.objects.filter(workspace=OuterRef("pk"))),
192
+ pk=workspace_id,
193
+ ).exists()
194
+
195
+
196
+ def auto_approved_connection(
197
+ workspace_id: int, connection_id: int | None, kind: str | None, binding: str | None
198
+ ) -> Connection | None:
199
+ """The connection while it is connected and needs no owner's review."""
200
+ return Connection.objects.filter(
201
+ pk=connection_id,
202
+ workspace_id=workspace_id,
203
+ kind=kind,
204
+ binding_key=binding,
205
+ status=Connection.Status.CONNECTED,
206
+ require_approval=False,
207
+ ).first()
208
+
209
+
210
+ # Sign-ins
211
+
212
+
213
+ def delete_stale_authorizations(user: AbstractBaseUser, before: datetime) -> None:
214
+ OAuthAuthorization.objects.filter(user=user, created_at__lt=before).delete()
215
+
216
+
217
+ def create_authorization(**fields: Any) -> OAuthAuthorization:
218
+ return OAuthAuthorization.objects.create(**fields)
219
+
220
+
221
+ def lock_unused_authorization(
222
+ state: str, user: AbstractBaseUser
223
+ ) -> OAuthAuthorization | None:
224
+ """Lock the person's unused sign-in with this state, with its connection."""
225
+ return (
226
+ # Lock only this row. Postgres cannot lock the optional connection join.
227
+ OAuthAuthorization.objects.select_for_update(of=("self",))
228
+ .select_related("connection")
229
+ .filter(state=state, user=user, used_at__isnull=True)
230
+ .first()
231
+ )
232
+
233
+
234
+ # Agent logins
235
+
236
+
237
+ def workspace_agent_connections(workspace: Workspace) -> list[AgentConnection]:
238
+ return list(AgentConnection.objects.filter(workspace=workspace))
239
+
240
+
241
+ def own_agent_connections(
242
+ workspace: Workspace, user: AbstractBaseUser
243
+ ) -> QuerySet[AgentConnection]:
244
+ return AgentConnection.objects.filter(workspace=workspace, owner=user)
245
+
246
+
247
+ def connected_logins(
248
+ workspace: Workspace, user: AbstractBaseUser
249
+ ) -> QuerySet[AgentConnection]:
250
+ """The person's own agent logins in the workspace that are signed in."""
251
+ return AgentConnection.objects.filter(
252
+ workspace=workspace, owner=user, connected=True
253
+ )
254
+
255
+
256
+ def find_agent_connection(
257
+ connection_id: int,
258
+ workspace_id: int,
259
+ owner_id: int,
260
+ credential_key: str,
261
+ backend: str,
262
+ ) -> AgentConnection | None:
263
+ """The login a run planned to use, if it still matches the plan."""
264
+ return AgentConnection.objects.filter(
265
+ pk=connection_id,
266
+ workspace_id=workspace_id,
267
+ owner_id=owner_id,
268
+ credential_key=credential_key,
269
+ backend=backend,
270
+ ).first()
271
+
272
+
273
+ def queue_cleanup(connection: AgentConnection) -> AgentCredentialCleanup:
274
+ """Ask the runner to forget the login's cached credentials."""
275
+ cleanup, _ = AgentCredentialCleanup.objects.get_or_create(
276
+ credential_key=connection.credential_key,
277
+ defaults={"connection": connection},
278
+ )
279
+ return cleanup
280
+
281
+
282
+ def count_cleanup_attempt(cleanup: AgentCredentialCleanup) -> None:
283
+ AgentCredentialCleanup.objects.filter(pk=cleanup.pk).update(
284
+ attempts=F("attempts") + 1, updated_at=timezone.now()
285
+ )
286
+
287
+
288
+ def pending_cleanups(limit: int) -> list[AgentCredentialCleanup]:
289
+ """The oldest cleanups the runner has not done yet."""
290
+ return list(AgentCredentialCleanup.objects.order_by("pk")[:limit])
291
+
292
+
293
+ def has_pending_cleanup(connection: AgentConnection) -> bool:
294
+ return connection.credential_cleanups.exists()
295
+
296
+
297
+ def login_run_counts(workspace_id: int, since: datetime) -> Counter[int]:
298
+ """Runs per agent login of the workspace since then."""
299
+ return Counter(
300
+ dict(
301
+ AgentConnection.objects.filter(workspace_id=workspace_id)
302
+ .annotate(recent=Count("runs", filter=Q(runs__created_at__gte=since)))
303
+ .values_list("pk", "recent")
304
+ )
305
+ )
@@ -0,0 +1,13 @@
1
+ """Errors shared by every kind of external tool (MCP servers, HTTP APIs)."""
2
+
3
+
4
+ class ExternalToolError(ValueError):
5
+ """A policy or configuration failure. The message is safe to show."""
6
+
7
+
8
+ class ExternalRequestError(ExternalToolError):
9
+ """A remote failure the agent may report as unavailable evidence."""
10
+
11
+
12
+ class ToolArgumentError(ExternalToolError):
13
+ """The model sent arguments the tool refuses. It may fix them and try again."""