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 @@
1
+ """SecondShift: run repository agent skills safely, on schedule."""
@@ -0,0 +1,3 @@
1
+ from secondshift.cli import main
2
+
3
+ main()
secondshift/cli.py ADDED
@@ -0,0 +1,17 @@
1
+ """The `secondshift` command. It runs Django management commands.
2
+
3
+ Installed, it defaults to the local environment (`secondshift up`). A
4
+ checkout's manage.py defaults to dev.
5
+ """
6
+
7
+ import sys
8
+
9
+ from django.core.management import execute_from_command_line
10
+
11
+ from secondshift.config.app import select_settings
12
+ from secondshift.config.constants import Environment
13
+
14
+
15
+ def main(default_environment: Environment = Environment.LOCAL) -> None:
16
+ select_settings(default_environment)
17
+ execute_from_command_line(sys.argv)
File without changes
@@ -0,0 +1,23 @@
1
+ """Compose the public API from routers owned by each Django app."""
2
+
3
+ from django.http import HttpRequest, HttpResponse
4
+ from ninja import NinjaAPI
5
+
6
+ from secondshift.workspaces.access import AccessDenied
7
+
8
+ api = NinjaAPI(title="SecondShift API", version="1.0")
9
+ for app in (
10
+ "core",
11
+ "jobs",
12
+ "runs",
13
+ "triggers",
14
+ "workspaces",
15
+ "repositories",
16
+ "connections",
17
+ ):
18
+ api.add_router("", f"secondshift.{app}.api.router")
19
+
20
+
21
+ @api.exception_handler(AccessDenied)
22
+ def access_denied(request: HttpRequest, error: AccessDenied) -> HttpResponse:
23
+ return api.create_response(request, {"detail": str(error)}, status=403)
@@ -0,0 +1,62 @@
1
+ """Server entry points for the web app, the model gateway and the runner.
2
+
3
+ Start an HTTP role with SECONDSHIFT_SERVICE set to web, gateway or runner:
4
+
5
+ gunicorn --config python:secondshift.config.gunicorn
6
+ """
7
+
8
+ import os
9
+
10
+ from django.core.asgi import get_asgi_application
11
+ from django.core.handlers.asgi import ASGIHandler
12
+ from django.core.handlers.wsgi import WSGIHandler
13
+ from django.core.wsgi import get_wsgi_application
14
+
15
+ from secondshift.config.constants import (
16
+ ENVIRONMENT_VAR,
17
+ RUNNER_SETTINGS,
18
+ SERVICE_VAR,
19
+ SETTINGS_MODULE_VAR,
20
+ Environment,
21
+ Service,
22
+ settings_module,
23
+ )
24
+
25
+
26
+ def select_settings(default: Environment) -> None:
27
+ """Use SECONDSHIFT_ENV, or the default, to pick the settings module."""
28
+ environment = Environment(os.environ.setdefault(ENVIRONMENT_VAR, default))
29
+ os.environ.setdefault(SETTINGS_MODULE_VAR, settings_module(environment))
30
+
31
+
32
+ def configure(service: Service) -> None:
33
+ os.environ[SERVICE_VAR] = service
34
+ if service == Service.RUNNER:
35
+ # Always its own settings, so it never loads the control plane's keys.
36
+ os.environ[SETTINGS_MODULE_VAR] = RUNNER_SETTINGS
37
+ else:
38
+ select_settings(default=Environment.PRODUCTION)
39
+
40
+
41
+ def started(service: Service) -> None:
42
+ if service == Service.RUNNER:
43
+ # Imported here because it needs settings, which are chosen above.
44
+ from secondshift.runner.services import get_runner_service # noqa: PLC0415
45
+
46
+ get_runner_service().verify_configuration()
47
+
48
+
49
+ def wsgi(name: str = Service.WEB) -> WSGIHandler:
50
+ service = Service(name)
51
+ configure(service)
52
+ application = get_wsgi_application()
53
+ started(service)
54
+ return application
55
+
56
+
57
+ def asgi(name: str = Service.WEB) -> ASGIHandler:
58
+ service = Service(name)
59
+ configure(service)
60
+ application = get_asgi_application()
61
+ started(service)
62
+ return application
@@ -0,0 +1,279 @@
1
+ """Names shared by the settings modules, entry points and the launcher.
2
+
3
+ The runner's settings must never import the control plane's settings, which
4
+ read its keys. Values both need live here instead.
5
+ """
6
+
7
+ from enum import StrEnum
8
+ from urllib.parse import urlsplit
9
+
10
+ # Git object formats supported by repository snapshots and execution plans.
11
+ COMMIT_SHA_PATTERN = r"[0-9a-f]{40}|[0-9a-f]{64}"
12
+
13
+ EXECUTE_RUN_WORKFLOW = "secondshift.execute_run"
14
+ SCHEDULED_JOB_WORKFLOW = "secondshift.scheduled_job"
15
+ RETRY_NOTIFICATION_WORKFLOW = "secondshift.retry_notification"
16
+ RETRY_PULL_REQUEST_WORKFLOW = "secondshift.retry_pull_request"
17
+ RETRY_ISSUES_WORKFLOW = "secondshift.retry_issues"
18
+
19
+ ENVIRONMENT_VAR = "SECONDSHIFT_ENV"
20
+ SETTINGS_MODULE_VAR = "DJANGO_SETTINGS_MODULE"
21
+ SERVICE_VAR = "SECONDSHIFT_SERVICE"
22
+
23
+
24
+ GUNICORN_CONFIG = "python:secondshift.config.gunicorn"
25
+
26
+ # How agent containers reach services on the Docker host.
27
+ HOST_ALIAS = "host.docker.internal"
28
+ # `secondshift up` builds this tag from the installed package.
29
+ LOCAL_AGENT_TAG = "secondshift-agent:local"
30
+ LOCAL_GATEWAY_URL = "http://127.0.0.1:8001"
31
+
32
+
33
+ # The gateway's MCP endpoint for subscription agents. Deploy files spell it out too.
34
+ AGENT_BROKER_PATH = "gateway/agent/mcp"
35
+
36
+
37
+ def local_agent_broker_url(gateway_url: str) -> str:
38
+ """The gateway's agent broker, as agent containers on this host reach it."""
39
+ port = urlsplit(gateway_url).port or 80
40
+ return f"http://{HOST_ALIAS}:{port}/{AGENT_BROKER_PATH}"
41
+
42
+
43
+ class ArtifactStorage(StrEnum):
44
+ FILESYSTEM = "filesystem"
45
+ S3 = "s3"
46
+
47
+
48
+ class AppEnv(StrEnum):
49
+ AGENT_IMAGE = "SECONDSHIFT_AGENT_IMAGE"
50
+ AGENT_RESOLV_CONF = "SECONDSHIFT_AGENT_RESOLV_CONF"
51
+ AGENT_NETWORK = "SECONDSHIFT_AGENT_NETWORK"
52
+ AGENT_BROKER_URL = "SECONDSHIFT_AGENT_BROKER_URL"
53
+ DATA_DIR = "SECONDSHIFT_DATA_DIR"
54
+ RUNNER_RUNTIME = "SECONDSHIFT_RUNNER_RUNTIME"
55
+ RUNNER_IMAGE = "SECONDSHIFT_RUNNER_IMAGE"
56
+ RUNNER_SERVICE_URL = "SECONDSHIFT_RUNNER_SERVICE_URL"
57
+ RUNNER_SERVICE_TOKEN = "SECONDSHIFT_RUNNER_SERVICE_TOKEN"
58
+ MODEL_GATEWAY_URL = "SECONDSHIFT_MODEL_GATEWAY_URL"
59
+ RUNNER_WORK_ROOT = "SECONDSHIFT_RUNNER_WORK_ROOT"
60
+ SOURCE_CACHE_ROOT = "SECONDSHIFT_SOURCE_CACHE_ROOT"
61
+ SOURCE_CACHE_MAX_BYTES = "SECONDSHIFT_SOURCE_CACHE_MAX_BYTES"
62
+ EXTERNAL_SKILLS_ENABLED = "SECONDSHIFT_EXTERNAL_SKILLS_ENABLED"
63
+ ARTIFACT_STORAGE = "SECONDSHIFT_ARTIFACT_STORAGE"
64
+ RUN_OUTPUT_MAX_BYTES = "SECONDSHIFT_RUN_OUTPUT_MAX_BYTES"
65
+ RUN_RETENTION_DAYS = "SECONDSHIFT_RUN_RETENTION_DAYS"
66
+ REPOSITORY_SYNC_INTERVAL_SECONDS = "SECONDSHIFT_REPOSITORY_SYNC_INTERVAL_SECONDS"
67
+
68
+ SECRET_KEY = "SECONDSHIFT_SECRET_KEY"
69
+ ENCRYPTION_KEY = "SECONDSHIFT_ENCRYPTION_KEY"
70
+ DATABASE_URL = "SECONDSHIFT_DATABASE_URL"
71
+ CACHE_URL = "SECONDSHIFT_CACHE_URL"
72
+ PUBLIC_URL = "SECONDSHIFT_PUBLIC_URL"
73
+ DEBUG = "SECONDSHIFT_DEBUG"
74
+ ALLOWED_HOSTS = "SECONDSHIFT_ALLOWED_HOSTS"
75
+ CSRF_TRUSTED_ORIGINS = "SECONDSHIFT_CSRF_TRUSTED_ORIGINS"
76
+ LOG_LEVEL = "SECONDSHIFT_LOG_LEVEL"
77
+ DB_CONN_MAX_AGE_SECONDS = "SECONDSHIFT_DB_CONN_MAX_AGE_SECONDS"
78
+ WORKFLOW_DATABASE_URL = "SECONDSHIFT_WORKFLOW_DATABASE_URL"
79
+ RUN_CONCURRENCY = "SECONDSHIFT_RUN_CONCURRENCY"
80
+ RUN_GLOBAL_CONCURRENCY = "SECONDSHIFT_RUN_GLOBAL_CONCURRENCY"
81
+ TASK_QUEUE_LIMIT = "SECONDSHIFT_TASK_QUEUE_LIMIT"
82
+ RUNNER_ALLOWED_HOSTS = "SECONDSHIFT_RUNNER_ALLOWED_HOSTS"
83
+ RUNNER_DATABASE_URL = "SECONDSHIFT_RUNNER_DATABASE_URL"
84
+ RUNNER_DB_CONN_MAX_AGE_SECONDS = "SECONDSHIFT_RUNNER_DB_CONN_MAX_AGE_SECONDS"
85
+ EMAIL_URL = "SECONDSHIFT_EMAIL_URL"
86
+ FROM_EMAIL = "SECONDSHIFT_FROM_EMAIL"
87
+
88
+ HTTP_BIND = "SECONDSHIFT_HTTP_BIND"
89
+ HTTP_PROCESSES = "SECONDSHIFT_HTTP_PROCESSES"
90
+ HTTP_TIMEOUT_SECONDS = "SECONDSHIFT_HTTP_TIMEOUT_SECONDS"
91
+ HTTP_GRACEFUL_TIMEOUT_SECONDS = "SECONDSHIFT_HTTP_GRACEFUL_TIMEOUT_SECONDS"
92
+ HTTP_LOG_LEVEL = "SECONDSHIFT_HTTP_LOG_LEVEL"
93
+ HTTP_ACCESS_LOG = "SECONDSHIFT_HTTP_ACCESS_LOG"
94
+
95
+
96
+ GITHUB_HOST = "github.com"
97
+ GITHUB_BASE_URL = f"https://{GITHUB_HOST}"
98
+ GITHUB_SSH_PREFIX = f"git@{GITHUB_HOST}:"
99
+ GITHUB_REPOSITORY_PATTERN = r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"
100
+ GITHUB_API_URL = "https://api.github.com"
101
+ GITHUB_OAUTH_URL = f"{GITHUB_BASE_URL}/login/oauth"
102
+ # The REST API version every request asks for.
103
+ GITHUB_API_VERSION = "2022-11-28"
104
+ # Fine-grained tokens are limited to chosen repositories. Classic tokens are not.
105
+ GITHUB_FINE_GRAINED_TOKEN_PATTERN = r"github_pat_[A-Za-z0-9_]+"
106
+ GITHUB_TOKEN_HINT = "Enter a fine-grained GitHub token. It starts with github_pat_."
107
+ # Limits for the files of one commit that a run gets. The control plane checks
108
+ # them while it builds a snapshot, and the runner checks them again.
109
+ MAX_SOURCE_SNAPSHOT_BYTES = 64 * 1024 * 1024
110
+ MAX_SOURCE_UNPACKED_BYTES = 1024 * 1024 * 1024
111
+ MAX_SOURCE_FILES = 200_000
112
+ # Limits for what a run leaves behind: the agent's file changes, and the files
113
+ # it saves in /outputs. The runner applies them, and the control plane again.
114
+ MAX_CHANGES_BYTES = 20 * 1024 * 1024
115
+ MAX_OUTPUT_FILES = 100
116
+ MAX_OUTPUT_BYTES = 100 * 1024 * 1024
117
+ # Earlier runs' saved files that one run gets back. Uploaded like a snapshot,
118
+ # so it stays under that limit even when the files do not compress.
119
+ MAX_HISTORY_FILES_BYTES = 48 * 1024 * 1024
120
+ # Issues one run may open. A weekly review should file a few, not dozens.
121
+ MAX_RUN_ISSUES = 10
122
+ # Where a sandbox finds the folder for files it wants to hand back.
123
+ SANDBOX_OUTPUTS_DIR = "/outputs"
124
+ # Where a sandbox finds the files its job's earlier runs saved. Read-only.
125
+ SANDBOX_HISTORY_DIR = "/history"
126
+ # Where a sandbox finds a skill from another repository. Read-only.
127
+ SANDBOX_SKILL_DIR = "/skill"
128
+ # Limits for a skill from another repository: its SKILL.md and the files beside it.
129
+ MAX_SKILL_FILES = 500
130
+ MAX_SKILL_UNPACKED_BYTES = 20 * 1024 * 1024
131
+ MAX_SKILL_SNAPSHOT_BYTES = 10 * 1024 * 1024
132
+ GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/readonly"
133
+ # The model gateway refuses a run past its token budget with this message.
134
+ TOKEN_BUDGET_MESSAGE = "This run has used its token budget."
135
+
136
+
137
+ class TriggerKind(StrEnum):
138
+ """Events a repository may ask to start a job. Never a URL or a secret."""
139
+
140
+ WEBHOOK = "webhook"
141
+
142
+
143
+ class ModelFamily(StrEnum):
144
+ """The API shape a model provider speaks. Names the gateway's URL path."""
145
+
146
+ ANTHROPIC = "anthropic"
147
+ OPENAI = "openai"
148
+
149
+
150
+ class RunStart(StrEnum):
151
+ """What started a run."""
152
+
153
+ SCHEDULE = "schedule"
154
+ MANUAL = "manual"
155
+ WEBHOOK = "webhook"
156
+ MCP = "mcp"
157
+
158
+
159
+ # How much of a report a run keeps as its summary. Webhooks send the summary.
160
+ SUMMARY_MAX_CHARS = 4_000
161
+
162
+
163
+ class GitHubMCPTool(StrEnum):
164
+ """Read tools for pull requests, commits, and Actions runs.
165
+
166
+ Actions tools that trigger runs, list artifacts, or return download links
167
+ are left out or blocked by method.
168
+ """
169
+
170
+ SEARCH_PULL_REQUESTS = "search_pull_requests"
171
+ PULL_REQUEST_READ = "pull_request_read"
172
+ LIST_COMMITS = "list_commits"
173
+ GET_COMMIT = "get_commit"
174
+ ACTIONS_LIST = "actions_list"
175
+ ACTIONS_GET = "actions_get"
176
+ GET_JOB_LOGS = "get_job_logs"
177
+
178
+
179
+ GITHUB_MCP_TOOLS = tuple(GitHubMCPTool)
180
+
181
+ SENTRY_MCP_URL = "https://mcp.sentry.dev/mcp"
182
+ # Sentry organization and project slugs.
183
+ SENTRY_SLUG_PATTERN = r"[a-z0-9][a-z0-9_\-]{0,63}"
184
+
185
+
186
+ class SentryMCPTool(StrEnum):
187
+ """Read tools for errors, issues, and traces.
188
+
189
+ The search tools pass their query to Sentry's own search agent. Replays,
190
+ logs, and profiles are left out because they often hold personal data.
191
+ """
192
+
193
+ FIND_PROJECTS = "find_projects"
194
+ SEARCH_ISSUES = "search_issues"
195
+ SEARCH_ERRORS = "search_errors"
196
+ SEARCH_TRACES = "search_traces"
197
+ GET_SENTRY_RESOURCE = "get_sentry_resource"
198
+
199
+
200
+ SENTRY_MCP_TOOLS = tuple(SentryMCPTool)
201
+
202
+
203
+ class Environment(StrEnum):
204
+ """Where SecondShift runs. Each one has a settings module of the same name."""
205
+
206
+ PRODUCTION = "production"
207
+ LOCAL = "local"
208
+ DEV = "dev"
209
+ TEST = "test"
210
+
211
+
212
+ class Service(StrEnum):
213
+ """The separately started parts of SecondShift."""
214
+
215
+ WEB = "web"
216
+ GATEWAY = "gateway"
217
+ RUNNER = "runner"
218
+ WORKER = "worker"
219
+
220
+
221
+ def settings_module(name: str) -> str:
222
+ return f"secondshift.config.settings.{name}"
223
+
224
+
225
+ class SandboxRuntime(StrEnum):
226
+ """The Docker runtime that runs command sandboxes."""
227
+
228
+ DOCKER = "docker"
229
+ GVISOR = "runsc"
230
+
231
+
232
+ class RunSandbox(StrEnum):
233
+ """What a run records as its sandbox."""
234
+
235
+ PENDING = "pending-runner-service"
236
+ ISOLATED_RUNNER = "isolated-runner"
237
+
238
+
239
+ RUNNER_SETTINGS = settings_module(Service.RUNNER)
240
+
241
+ # Docker's official Python image, pinned by digest so a rebuilt tag can't change
242
+ # what runs. The agent's tools need only `find`, `cat` and `grep`. Git runs in
243
+ # the runner, not the sandbox. Operators override it with SECONDSHIFT_RUNNER_IMAGE.
244
+ # To update, run `docker buildx imagetools inspect python:3.13-slim` and copy
245
+ # the index digest.
246
+ DEFAULT_SANDBOX_IMAGE = (
247
+ "python:3.13-slim"
248
+ "@sha256:57b29a7c050c4e679e4bea572e14b3965cf4b5839de2da49d55e49ffc4024f39"
249
+ )
250
+
251
+ # The repository file that lists jobs, at the repository root.
252
+ MANIFEST_FILE = ".secondshift.yaml"
253
+
254
+ # How much of a skill file a job keeps for people to read in the dashboard.
255
+ MAX_SKILL_PREVIEW_CHARS = 200_000
256
+
257
+
258
+ class AgentBackend(StrEnum):
259
+ """Who runs the agent: SecondShift's own loop, or a subscription CLI."""
260
+
261
+ PYDANTIC = "pydantic"
262
+ CLAUDE_CODE = "claude_code"
263
+ CODEX = "codex"
264
+
265
+ @property
266
+ def uses_model_api(self) -> bool:
267
+ """Runs call a model provider through SecondShift's gateway. The CLIs
268
+ call their provider directly with the subscription login."""
269
+ return self is AgentBackend.PYDANTIC
270
+
271
+ @property
272
+ def starts_subagents(self) -> bool:
273
+ """Runs can start sub-agents when the job allows them."""
274
+ return self is not AgentBackend.CODEX
275
+
276
+ @property
277
+ def limits_model_calls(self) -> bool:
278
+ """Runs stop at the run's model-call limit."""
279
+ return self is not AgentBackend.CODEX
@@ -0,0 +1,118 @@
1
+ """Generate private per-role env files for a new container installation."""
2
+
3
+ import argparse
4
+ import os
5
+ import re
6
+ import secrets
7
+ from pathlib import Path
8
+
9
+ from cryptography.fernet import Fernet
10
+
11
+ from secondshift.config.constants import (
12
+ ENVIRONMENT_VAR,
13
+ SERVICE_VAR,
14
+ AppEnv,
15
+ Environment,
16
+ SandboxRuntime,
17
+ Service,
18
+ )
19
+
20
+ ROLE_FILES = {role: f"{role}.env" for role in Service}
21
+
22
+
23
+ def generate_environment(hostname: str, directory: Path) -> None:
24
+ if len(hostname) > 253 or not all(
25
+ re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?", label)
26
+ for label in hostname.split(".")
27
+ ):
28
+ raise ValueError(
29
+ "Use a DNS hostname without a scheme, port, path or credentials"
30
+ )
31
+ directory = directory.resolve()
32
+ directory.mkdir(parents=True, exist_ok=True)
33
+ # Never overwrite existing configuration or replace its keys.
34
+ if any((directory / name).exists() for name in [*ROLE_FILES.values(), ".env"]):
35
+ raise FileExistsError(
36
+ "Configuration already exists; reuse it rather than generating new keys"
37
+ )
38
+ common = {
39
+ AppEnv.SECRET_KEY: secrets.token_urlsafe(48),
40
+ AppEnv.ENCRYPTION_KEY: Fernet.generate_key().decode(),
41
+ AppEnv.DATABASE_URL: "sqlite:////var/lib/secondshift/control.sqlite3",
42
+ }
43
+ public = {
44
+ AppEnv.PUBLIC_URL: f"https://{hostname.lower()}",
45
+ AppEnv.RUNNER_RUNTIME: SandboxRuntime.GVISOR,
46
+ }
47
+ token = secrets.token_urlsafe(48)
48
+ runner = directory / "runner"
49
+ runner.mkdir(mode=0o700, exist_ok=True)
50
+ configs = {
51
+ Service.WEB: {
52
+ **common,
53
+ **public,
54
+ AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
55
+ AppEnv.RUNNER_SERVICE_TOKEN: token,
56
+ AppEnv.HTTP_TIMEOUT_SECONDS: "300",
57
+ },
58
+ Service.GATEWAY: {
59
+ **common,
60
+ # The agent broker runs workspace tools for subscription runs.
61
+ AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
62
+ AppEnv.RUNNER_SERVICE_TOKEN: token,
63
+ AppEnv.ALLOWED_HOSTS: "secondshift-gateway",
64
+ AppEnv.HTTP_BIND: "0.0.0.0:8001",
65
+ AppEnv.HTTP_GRACEFUL_TIMEOUT_SECONDS: "600",
66
+ },
67
+ Service.WORKER: {
68
+ **common,
69
+ **public,
70
+ AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
71
+ AppEnv.RUNNER_SERVICE_TOKEN: token,
72
+ AppEnv.MODEL_GATEWAY_URL: "http://secondshift-gateway:8001",
73
+ },
74
+ Service.RUNNER: {
75
+ AppEnv.HTTP_BIND: "0.0.0.0:8002",
76
+ AppEnv.HTTP_PROCESSES: "1",
77
+ AppEnv.HTTP_TIMEOUT_SECONDS: "180",
78
+ AppEnv.RUNNER_ALLOWED_HOSTS: "secondshift-runner",
79
+ AppEnv.RUNNER_SERVICE_TOKEN: token,
80
+ AppEnv.RUNNER_RUNTIME: SandboxRuntime.GVISOR,
81
+ AppEnv.DATA_DIR: str(runner),
82
+ AppEnv.RUNNER_WORK_ROOT: str(runner),
83
+ },
84
+ }
85
+ for role, values in configs.items():
86
+ descriptor = os.open(
87
+ directory / ROLE_FILES[role], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
88
+ )
89
+ with os.fdopen(descriptor, "w") as output:
90
+ output.write(f"{ENVIRONMENT_VAR}={Environment.PRODUCTION}\n")
91
+ output.write(f"{SERVICE_VAR}={role}\n")
92
+ for name, value in values.items():
93
+ output.write(f"{name}={value}\n")
94
+
95
+
96
+ def main() -> None:
97
+ parser = argparse.ArgumentParser(description=__doc__)
98
+ parser.add_argument(
99
+ "--hostname", required=True, help="Public DNS hostname, without https://"
100
+ )
101
+ parser.add_argument(
102
+ "--directory",
103
+ type=Path,
104
+ default=Path.cwd(),
105
+ help="Output directory (default: current directory)",
106
+ )
107
+ args = parser.parse_args()
108
+ try:
109
+ generate_environment(args.hostname, args.directory)
110
+ except (ValueError, OSError) as error:
111
+ parser.exit(1, f"{error}\n")
112
+ print(
113
+ "Created web.env, gateway.env, worker.env and runner.env. Keep these files private."
114
+ )
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
@@ -0,0 +1,40 @@
1
+ """First start of a development install: create the data folder and its keys."""
2
+
3
+ import fcntl
4
+ import os
5
+ import secrets
6
+ from pathlib import Path
7
+
8
+ from cryptography.fernet import Fernet
9
+
10
+ from secondshift.config.constants import AppEnv
11
+
12
+
13
+ def write_generated_env(data_dir: Path) -> None:
14
+ """Create the data folder with fresh keys in its .env, once.
15
+
16
+ The runner gets only its token, in runner.env. A lock keeps processes
17
+ starting together from writing different keys.
18
+ """
19
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
20
+ with open(data_dir / ".lock", "w") as lock:
21
+ fcntl.flock(lock, fcntl.LOCK_EX)
22
+ if (data_dir / ".env").exists():
23
+ return
24
+ token = secrets.token_urlsafe(48)
25
+ files = {
26
+ "runner.env": f"SECONDSHIFT_RUNNER_SERVICE_TOKEN={token}\n",
27
+ ".env": (
28
+ "# Generated on first start. Back it up: SECONDSHIFT_ENCRYPTION_KEY\n"
29
+ "# decrypts stored connection credentials.\n"
30
+ f"{AppEnv.SECRET_KEY}={secrets.token_urlsafe(48)}\n"
31
+ f"{AppEnv.ENCRYPTION_KEY}={Fernet.generate_key().decode()}\n"
32
+ f"SECONDSHIFT_RUNNER_SERVICE_TOKEN={token}\n"
33
+ ),
34
+ }
35
+ for name, content in files.items(): # .env last: it marks completion
36
+ descriptor = os.open(
37
+ data_dir / name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600
38
+ )
39
+ with os.fdopen(descriptor, "w") as file:
40
+ file.write(content)
@@ -0,0 +1,5 @@
1
+ def duration_label(seconds: int) -> str:
2
+ if seconds % 60 == 0:
3
+ minutes = seconds // 60
4
+ return f"{minutes} minute" if minutes == 1 else f"{minutes} minutes"
5
+ return f"{seconds} seconds"
@@ -0,0 +1,31 @@
1
+ """Shared Gunicorn options, configured by each process environment."""
2
+
3
+ import os
4
+
5
+ from environ import Env
6
+
7
+ from secondshift.config.constants import SERVICE_VAR, AppEnv, Service
8
+
9
+ service = Service(os.environ.get(SERVICE_VAR, Service.WEB))
10
+ if service == Service.WORKER:
11
+ raise ValueError(
12
+ "The worker role runs python manage.py run_dbos_worker, not Gunicorn"
13
+ )
14
+ env = Env()
15
+
16
+ # Both hold long requests open (model streams, agent runs) while serving others.
17
+ async_http = service in (Service.GATEWAY, Service.RUNNER)
18
+ factory = "asgi" if async_http else "wsgi"
19
+ wsgi_app = f"secondshift.config.app:{factory}('{service}')"
20
+ worker_class = "uvicorn_worker.UvicornWorker" if async_http else "sync"
21
+ # One address, or several separated by commas.
22
+ bind = env.list(AppEnv.HTTP_BIND, default=["0.0.0.0:8000"])
23
+ workers = env.int(AppEnv.HTTP_PROCESSES, default=2)
24
+ timeout = env.int(AppEnv.HTTP_TIMEOUT_SECONDS, default=30)
25
+ graceful_timeout = env.int(AppEnv.HTTP_GRACEFUL_TIMEOUT_SECONDS, default=30)
26
+ loglevel = env.str(AppEnv.HTTP_LOG_LEVEL, default="info").lower()
27
+ accesslog = env.str(AppEnv.HTTP_ACCESS_LOG, default="-") or None
28
+ if workers < 1 or timeout < 1 or graceful_timeout < 1:
29
+ raise ValueError("Gunicorn worker count and timeouts must be positive")
30
+ # Services may share a data directory. Never compete for a shared control socket.
31
+ control_socket_disable = True
@@ -0,0 +1,40 @@
1
+ """Shared logging policy for the control plane and isolated runner."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def logging_config(level: str, service: str) -> dict[str, Any]:
7
+ """Route application and Django records through one console handler."""
8
+ return {
9
+ "version": 1,
10
+ "disable_existing_loggers": False,
11
+ "formatters": {
12
+ "console": {
13
+ "format": (
14
+ "{asctime} {levelname} " + service + "[{process}] {name}: {message}"
15
+ ),
16
+ "style": "{",
17
+ }
18
+ },
19
+ "handlers": {
20
+ "console": {"class": "logging.StreamHandler", "formatter": "console"},
21
+ "null": {"class": "logging.NullHandler"},
22
+ },
23
+ "root": {"handlers": ["console"], "level": level},
24
+ "loggers": {
25
+ # Replace Django's default handlers to avoid duplicate output and
26
+ # let LOG_LEVEL apply to requests and development server messages.
27
+ "django": {"handlers": [], "level": "NOTSET", "propagate": True},
28
+ "django.server": {
29
+ "handlers": [],
30
+ "level": "NOTSET",
31
+ "propagate": True,
32
+ },
33
+ # Request URLs may contain provider or webhook credentials.
34
+ "httpx": {"level": "WARNING"},
35
+ "httpcore": {"level": "WARNING"},
36
+ # The SDK can log raw payloads and session identifiers. The broker
37
+ # emits bounded audit events and sanitized errors instead.
38
+ "mcp": {"handlers": ["null"], "propagate": False},
39
+ },
40
+ }
File without changes