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,100 @@
1
+ from secondshift.connections.execution import recent_login_runs
2
+ from secondshift.connections.github import is_github_source, shows_attribution
3
+ from secondshift.connections.mcp.base import SOURCE_AUTH
4
+ from secondshift.connections.mcp.oauth import requested_scopes
5
+ from secondshift.connections.models import AgentConnection, Connection
6
+ from secondshift.connections.schemas import (
7
+ ConnectionSchema,
8
+ ConnectionUsageSchema,
9
+ OfferedToolSchema,
10
+ )
11
+ from secondshift.connections.services import (
12
+ connection_target,
13
+ )
14
+ from secondshift.runs.reporting.connection_usage import ConnectionUsage
15
+
16
+
17
+ def connection_settings_view(connection: Connection) -> dict[str, str]:
18
+ """Owner-chosen settings that are safe to show. Never a credential."""
19
+ if connection.kind == Connection.Kind.MCP:
20
+ return connection.settings
21
+ if connection.kind == Connection.Kind.API:
22
+ return {
23
+ **connection.settings,
24
+ "base_url": connection.endpoint_url,
25
+ "spec_url": connection.metadata.get("spec_url", ""),
26
+ }
27
+ return {}
28
+
29
+
30
+ def connection_auth(connection: Connection) -> str:
31
+ """How the connection signs in: oauth, token, app, or empty."""
32
+ if connection.kind == Connection.Kind.SOURCE:
33
+ return (
34
+ "app"
35
+ if connection.auth_strategy == Connection.AuthStrategy.MANAGED
36
+ else "token"
37
+ )
38
+ if connection.kind not in {Connection.Kind.MCP, Connection.Kind.API}:
39
+ return ""
40
+ if connection.auth_strategy == Connection.AuthStrategy.OAUTH:
41
+ return "oauth"
42
+ if connection.auth_strategy == Connection.AuthStrategy.MANAGED:
43
+ return SOURCE_AUTH
44
+ return "token"
45
+
46
+
47
+ def serialize_connection(
48
+ connection: Connection, usage: ConnectionUsage | None = None
49
+ ) -> ConnectionSchema:
50
+ return ConnectionSchema(
51
+ id=connection.id,
52
+ provider=connection.provider,
53
+ kind=connection.kind,
54
+ binding_key=connection.binding_key,
55
+ display_name=connection.display_name,
56
+ status=connection.current_status,
57
+ expires_at=connection.expires_at,
58
+ available_tools=connection.available_tools,
59
+ target=connection_target(connection),
60
+ allowed_models=connection.allowed_models,
61
+ default_model=connection.default_model,
62
+ is_default=connection.kind == Connection.Kind.MODEL
63
+ and connection.workspace.default_model_connection_id == connection.pk,
64
+ available_models=list(connection.metadata.get("available_models", [])),
65
+ last_check=connection.last_check,
66
+ settings=connection_settings_view(connection),
67
+ auth=connection_auth(connection),
68
+ scopes=list(connection.scopes)
69
+ if connection.kind == Connection.Kind.MCP
70
+ else [],
71
+ requested_scopes=requested_scopes(connection)
72
+ if connection.kind == Connection.Kind.MCP
73
+ and connection.auth_strategy == Connection.AuthStrategy.OAUTH
74
+ else [],
75
+ offered_tools=[
76
+ OfferedToolSchema(
77
+ name=name,
78
+ description=str(tool.get("description", ""))[:500],
79
+ read_only=bool(tool.get("readOnly")),
80
+ )
81
+ for name, tool in sorted(connection.offered_tools.items())
82
+ ],
83
+ require_approval=connection.require_approval,
84
+ attribution=is_github_source(connection) and shows_attribution(connection),
85
+ usage=None
86
+ if usage is None
87
+ else ConnectionUsageSchema(
88
+ **{**vars(usage), "spend_7d_usd": float(usage.spend_7d_usd)}
89
+ ),
90
+ )
91
+
92
+
93
+ def with_recent_runs(
94
+ connections: list[AgentConnection], workspace_id: int
95
+ ) -> list[AgentConnection]:
96
+ """Attach each login's recent run count for AgentConnectionSchema."""
97
+ counts = recent_login_runs(workspace_id)
98
+ for connection in connections:
99
+ connection.recent_runs = counts[connection.pk]
100
+ return connections
@@ -0,0 +1,94 @@
1
+ from django.db import IntegrityError, transaction
2
+
3
+ from secondshift.connections.dao import binding_taken
4
+ from secondshift.connections.models import Connection
5
+ from secondshift.connections.outbound import host_label
6
+ from secondshift.connections.webhooks import validate_webhook_url
7
+ from secondshift.workspaces.models import Workspace
8
+
9
+
10
+ class ConnectionConflict(ValueError):
11
+ pass
12
+
13
+
14
+ def connection_target(connection: Connection) -> str:
15
+ """The host a connection reaches, as saved when it was set up."""
16
+ return str(connection.metadata.get("target", ""))
17
+
18
+
19
+ def create_webhook_connection(
20
+ workspace: Workspace,
21
+ binding_key: str,
22
+ display_name: str,
23
+ url: str,
24
+ ) -> Connection:
25
+ url = validate_webhook_url(url)
26
+ if binding_taken(workspace.pk, binding_key):
27
+ raise ConnectionConflict(
28
+ f"A connection for {binding_key} already exists in this workspace."
29
+ )
30
+ connection = Connection(
31
+ workspace=workspace,
32
+ provider=Connection.Provider.WEBHOOK,
33
+ kind=Connection.Kind.NOTIFICATION,
34
+ binding_key=binding_key,
35
+ display_name=display_name.strip() or binding_key,
36
+ auth_strategy=Connection.AuthStrategy.NONE,
37
+ delivery_url=url,
38
+ metadata={"target": host_label(url)},
39
+ )
40
+ connection.full_clean()
41
+ try:
42
+ with transaction.atomic():
43
+ connection.save()
44
+ except IntegrityError as error:
45
+ raise ConnectionConflict(
46
+ f"A connection for {binding_key} already exists in this workspace."
47
+ ) from error
48
+ return connection
49
+
50
+
51
+ def update_webhook_connection(
52
+ connection: Connection,
53
+ display_name: str | None,
54
+ url: str | None,
55
+ ) -> Connection:
56
+ if connection.provider != Connection.Provider.WEBHOOK:
57
+ raise ConnectionConflict("Only webhook connections can be edited here.")
58
+ if display_name is not None:
59
+ connection.display_name = display_name.strip() or connection.binding_key
60
+ if url:
61
+ url = validate_webhook_url(url)
62
+ connection.delivery_url = url
63
+ connection.metadata = {
64
+ **connection.metadata,
65
+ "target": host_label(url),
66
+ }
67
+ connection.status = Connection.Status.CONNECTED
68
+ elif connection.status == Connection.Status.REVOKED:
69
+ raise ConnectionConflict("Enter a new URL to reconnect this webhook.")
70
+ connection.full_clean()
71
+ connection.save()
72
+ return connection
73
+
74
+
75
+ def revoke_connection(connection: Connection) -> Connection:
76
+ """Stop all use immediately and discard stored credentials; keep the record."""
77
+ connection.status = Connection.Status.REVOKED
78
+ connection.delivery_url = ""
79
+ connection.access_token = ""
80
+ connection.refresh_token = ""
81
+ connection.metadata = {
82
+ key: value for key, value in connection.metadata.items() if key != "target"
83
+ }
84
+ connection.save(
85
+ update_fields=[
86
+ "status",
87
+ "delivery_url",
88
+ "access_token",
89
+ "refresh_token",
90
+ "metadata",
91
+ "updated_at",
92
+ ]
93
+ )
94
+ return connection
@@ -0,0 +1,38 @@
1
+ """What a connection's server offers, and which of those tools jobs may use."""
2
+
3
+ from typing import Any
4
+
5
+ from secondshift.connections.models import Connection
6
+
7
+
8
+ class UnknownTools(ValueError):
9
+ pass
10
+
11
+
12
+ def store_tools(
13
+ connection: Connection,
14
+ offered: dict[str, Any],
15
+ selected: list[str] | None,
16
+ *,
17
+ owner_selects: bool = True,
18
+ ) -> None:
19
+ """Save the offered tools and the ones jobs may use.
20
+
21
+ When the owner chooses, tools marked read-only start chosen. Otherwise
22
+ jobs may use every offered tool. Raises UnknownTools for a name the server
23
+ does not offer.
24
+ """
25
+ if not owner_selects:
26
+ chosen = set(offered)
27
+ elif selected is None:
28
+ chosen = {name for name, tool in offered.items() if tool.get("readOnly")}
29
+ elif set(selected) - set(offered):
30
+ raise UnknownTools
31
+ else:
32
+ chosen = set(selected)
33
+ connection.available_tools = sorted(chosen)
34
+ connection.metadata = {
35
+ **connection.metadata,
36
+ "tools": {name: offered[name] for name in sorted(chosen)},
37
+ "offered_tools": offered if owner_selects else {},
38
+ }
@@ -0,0 +1,107 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from django.conf import settings
7
+
8
+ from secondshift.connections.checks import record_check
9
+ from secondshift.connections.models import Connection
10
+ from secondshift.connections.outbound import (
11
+ OutboundUrlError,
12
+ PinnedSyncTransport,
13
+ validate_outbound_url,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ USER_AGENT = "SecondShift-Webhook/1"
19
+
20
+
21
+ WebhookError = OutboundUrlError
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class WebhookDelivery:
26
+ ok: bool
27
+ status_code: int | None
28
+ message: str
29
+ retryable: bool = False
30
+
31
+
32
+ def validate_webhook_url(url: str) -> str:
33
+ return validate_outbound_url(url, "webhook URL")
34
+
35
+
36
+ def webhook_transport() -> httpx.BaseTransport:
37
+ # Connects only to the addresses it checked, so DNS cannot rebind
38
+ # between the check and the request.
39
+ return PinnedSyncTransport()
40
+
41
+
42
+ def deliver_webhook(connection: Connection, payload: dict[str, Any]) -> WebhookDelivery:
43
+ """Send the payload and keep the result, so the dashboard shows the last delivery."""
44
+ delivery = post_webhook(connection, payload)
45
+ if connection.pk:
46
+ record_check(connection, delivery.ok, delivery.message)
47
+ return delivery
48
+
49
+
50
+ def post_webhook(connection: Connection, payload: dict[str, Any]) -> WebhookDelivery:
51
+ if (
52
+ connection.provider != Connection.Provider.WEBHOOK
53
+ or not connection.delivery_url
54
+ ):
55
+ return WebhookDelivery(False, None, "This connection has no webhook URL.")
56
+ try:
57
+ # Validated again at send time: DNS answers can change after saving.
58
+ url = validate_webhook_url(connection.delivery_url)
59
+ except WebhookError as error:
60
+ return WebhookDelivery(False, None, str(error))
61
+ try:
62
+ with httpx.Client(
63
+ transport=webhook_transport(),
64
+ timeout=settings.OUTBOUND_TIMEOUT_SECONDS,
65
+ follow_redirects=False,
66
+ ) as client:
67
+ response = client.post(
68
+ url, json=payload, headers={"User-Agent": USER_AGENT}
69
+ )
70
+ except httpx.TimeoutException:
71
+ return WebhookDelivery(False, None, "The webhook timed out.", retryable=True)
72
+ except httpx.HTTPError as error:
73
+ # Exception text includes the URL, which may embed a token.
74
+ logger.warning(
75
+ "webhook delivery to connection %s failed: %s",
76
+ connection.pk,
77
+ type(error).__name__,
78
+ )
79
+ return WebhookDelivery(
80
+ False, None, "The webhook could not be reached.", retryable=True
81
+ )
82
+ status = response.status_code
83
+ if 200 <= status < 300:
84
+ return WebhookDelivery(True, status, f"Delivered (HTTP {status}).")
85
+ if 300 <= status < 400:
86
+ return WebhookDelivery(
87
+ False,
88
+ status,
89
+ f"The webhook redirected (HTTP {status}); redirects are not followed.",
90
+ )
91
+ return WebhookDelivery(
92
+ False,
93
+ status,
94
+ f"The webhook answered HTTP {status}.",
95
+ retryable=status == 429 or status >= 500,
96
+ )
97
+
98
+
99
+ def connection_test_payload(connection: Connection) -> dict[str, Any]:
100
+ return {
101
+ "event": "connection.test",
102
+ "text": (
103
+ f"SecondShift test message for {connection.binding_key}. "
104
+ "Run reports for jobs using this binding will arrive here."
105
+ ),
106
+ "binding": connection.binding_key,
107
+ }
File without changes
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ from allauth.account.adapter import DefaultAccountAdapter
4
+ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
5
+ from allauth.socialaccount.models import SocialLogin
6
+ from django.conf import settings
7
+ from django.http import HttpRequest
8
+
9
+ from secondshift.workspaces.invites import pending_invite
10
+
11
+
12
+ def signup_open(request: HttpRequest) -> bool:
13
+ """Open to everyone when enabled, else only to someone with an invite."""
14
+ return settings.SIGNUP_ENABLED or pending_invite(request) is not None
15
+
16
+
17
+ class AccountAdapter(DefaultAccountAdapter):
18
+ def is_open_for_signup(self, request: HttpRequest) -> bool:
19
+ return signup_open(request)
20
+
21
+ def add_message(
22
+ self,
23
+ request: HttpRequest,
24
+ level: int,
25
+ message_template: str | None = None,
26
+ message_context: dict[str, Any] | None = None,
27
+ extra_tags: str = "",
28
+ message: str | None = None,
29
+ ) -> None:
30
+ """Drop the message: most flows end in the dashboard, which never
31
+ shows them, so they piled up and appeared stale on a later page."""
32
+
33
+
34
+ class SocialAccountAdapter(DefaultSocialAccountAdapter):
35
+ def is_open_for_signup(
36
+ self, request: HttpRequest, sociallogin: SocialLogin
37
+ ) -> bool:
38
+ return signup_open(request)
@@ -0,0 +1,5 @@
1
+ from django.contrib import admin
2
+
3
+ admin.site.site_header = "SecondShift Admin"
4
+ admin.site.site_title = "SecondShift Admin"
5
+ admin.site.index_title = "Administration"
@@ -0,0 +1,104 @@
1
+ from django.conf import settings
2
+ from django.http import HttpRequest
3
+ from django.utils import timezone
4
+ from ninja import Router
5
+ from ninja.security import django_auth
6
+
7
+ from secondshift.connections.dao import connected_count
8
+ from secondshift.connections.execution import execution_status
9
+ from secondshift.core.schemas import (
10
+ FailingJobSchema,
11
+ JobSpendSchema,
12
+ OverviewSchema,
13
+ RunDaySchema,
14
+ UserSchema,
15
+ )
16
+ from secondshift.github.app import configured as github_app_configured
17
+ from secondshift.jobs.dao import enabled_job_count
18
+ from secondshift.repositories.dao import active_repositories
19
+ from secondshift.repositories.models import Repository
20
+ from secondshift.runs.dao import run_counts_today
21
+ from secondshift.runs.reporting.health import (
22
+ failing_jobs,
23
+ runs_by_day,
24
+ spend_by_job,
25
+ week_summary,
26
+ )
27
+ from secondshift.workspaces.access import (
28
+ current_membership,
29
+ current_workspace,
30
+ is_operator,
31
+ permitted_actions,
32
+ )
33
+ from secondshift.workspaces.dao import has_own_token
34
+ from secondshift.workspaces.server_settings import (
35
+ environment_label,
36
+ sandbox_label,
37
+ )
38
+ from secondshift.workspaces.services import can_create_workspace
39
+
40
+ router = Router(auth=django_auth, tags=["core"])
41
+
42
+
43
+ @router.get("/me", response=UserSchema, auth=django_auth)
44
+ def me(request: HttpRequest) -> UserSchema:
45
+ membership = current_membership(request)
46
+ workspace = membership.workspace if membership else None
47
+ return UserSchema(
48
+ email=request.user.email or request.user.get_username(),
49
+ workspace_name=workspace.name if workspace else None,
50
+ workspace_slug=workspace.slug if workspace else None,
51
+ role=membership.role if membership else None,
52
+ permissions=permitted_actions(request),
53
+ can_use_admin=is_operator(request),
54
+ can_create_workspace=can_create_workspace(request.user),
55
+ local_repositories_enabled=settings.LOCAL_REPOSITORIES_ENABLED,
56
+ github_app_configured=github_app_configured(),
57
+ environment=environment_label(),
58
+ sandbox=sandbox_label(),
59
+ )
60
+
61
+
62
+ @router.get("/overview", response=OverviewSchema, auth=django_auth)
63
+ def overview(request: HttpRequest) -> OverviewSchema:
64
+ workspace = current_workspace(request)
65
+ if not workspace:
66
+ return OverviewSchema(
67
+ repositories=0,
68
+ local_repositories=0,
69
+ enabled_jobs=0,
70
+ runs_today=0,
71
+ failed_runs=0,
72
+ connected_services=0,
73
+ )
74
+ today = timezone.localdate()
75
+ repositories = active_repositories(workspace.pk)
76
+ counts = run_counts_today(workspace.pk, today)
77
+ week = week_summary(workspace)
78
+ return OverviewSchema(
79
+ repositories=repositories.count(),
80
+ local_repositories=repositories.filter(
81
+ source_type=Repository.SourceType.LOCAL
82
+ ).count(),
83
+ enabled_jobs=enabled_job_count(workspace.pk),
84
+ runs_today=counts["today"],
85
+ failed_runs=counts["failed"],
86
+ connected_services=connected_count(workspace.pk),
87
+ failing_jobs=[
88
+ FailingJobSchema(**vars(failing)) for failing in failing_jobs(workspace)
89
+ ],
90
+ runs_7d=week.runs,
91
+ runs_previous_7d=week.previous_runs,
92
+ finished_7d=week.finished,
93
+ succeeded_7d=week.succeeded,
94
+ spend_7d_usd=float(week.spend_usd),
95
+ spend_previous_7d_usd=float(week.previous_spend_usd),
96
+ spend_partial=week.spend_partial,
97
+ spend_by_job=[
98
+ JobSpendSchema(**{**vars(row), "spend_usd": float(row.spend_usd)})
99
+ for row in spend_by_job(workspace)
100
+ ],
101
+ run_days=[RunDaySchema(**vars(day)) for day in runs_by_day(workspace)],
102
+ execution=execution_status(workspace),
103
+ has_agent_token=has_own_token(request.user, workspace),
104
+ )
@@ -0,0 +1,20 @@
1
+ from typing import Any
2
+
3
+ from django.apps import AppConfig
4
+ from django.core.management import call_command
5
+ from django.db.models.signals import post_migrate
6
+
7
+
8
+ def create_cache_tables(using: str, **kwargs: Any) -> None:
9
+ """Create the database cache table, so every migrate leaves it ready."""
10
+ call_command("createcachetable", database=using, verbosity=0)
11
+
12
+
13
+ class CoreConfig(AppConfig):
14
+ default_auto_field = "django.db.models.BigAutoField"
15
+ name = "secondshift.core"
16
+
17
+ def ready(self) -> None:
18
+ # Django sends post_migrate only for apps with models, and core has
19
+ # none. Listen to every app. The command skips a table that exists.
20
+ post_migrate.connect(create_cache_tables, dispatch_uid="create_cache_tables")