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,524 @@
1
+ """Settings shared by every environment.
2
+
3
+ SECONDSHIFT_ENV picks the settings module. It is read from the process
4
+ environment, never from .env.
5
+
6
+ - production: a server. production.py requires every key and adds HTTPS.
7
+ - local: `secondshift up` on one machine for one person (local.py).
8
+ - dev: a checkout with the Vite dev server (dev.py).
9
+ - test: the test suite (test.py).
10
+
11
+ Defaults are safe for production. Values that differ on a single machine use
12
+ `default=SINGLE_HOST` or `default=DEV`.
13
+
14
+ Local mode must work with no settings beyond the keys made on first start. The
15
+ data folder's .env outlives upgrades, so local values belong in code.
16
+ """
17
+
18
+ import os
19
+ from datetime import timedelta
20
+ from pathlib import Path
21
+ from urllib.parse import urlsplit
22
+
23
+ from django.core.exceptions import ImproperlyConfigured
24
+ from django.utils.csp import CSP
25
+ from environ import Env
26
+
27
+ from secondshift.config.constants import (
28
+ DEFAULT_SANDBOX_IMAGE,
29
+ ENVIRONMENT_VAR,
30
+ GITHUB_BASE_URL,
31
+ LOCAL_AGENT_TAG,
32
+ LOCAL_GATEWAY_URL,
33
+ SERVICE_VAR,
34
+ AppEnv,
35
+ ArtifactStorage,
36
+ Environment,
37
+ SandboxRuntime,
38
+ Service,
39
+ local_agent_broker_url,
40
+ )
41
+ from secondshift.config.first_run import write_generated_env
42
+ from secondshift.config.logging import logging_config
43
+ from secondshift.config.static_storage import immutable_static_file
44
+
45
+ # The secondshift package, and the checkout that contains it.
46
+ PACKAGE_DIR = Path(__file__).resolve().parents[2]
47
+ BASE_DIR = PACKAGE_DIR.parent
48
+ # The built dashboard (npm --prefix dashboard run build).
49
+ WEB_DIR = PACKAGE_DIR / "web"
50
+
51
+ ENVIRONMENT = Environment(os.environ.get(ENVIRONMENT_VAR, Environment.PRODUCTION))
52
+ DEV = ENVIRONMENT == Environment.DEV
53
+ # One trusted person on one machine. Plain Docker, local repositories and
54
+ # generated keys are fine there.
55
+ SINGLE_HOST = ENVIRONMENT in {Environment.DEV, Environment.LOCAL}
56
+
57
+ env = Env()
58
+
59
+
60
+ # Real environment wins, then a checkout's .env, then the data folder's .env.
61
+ # A checkout's .env holds development settings. Local mode skips it so it runs
62
+ # the same as an installed copy. Tests skip it so they use the defaults.
63
+ if ENVIRONMENT not in {Environment.LOCAL, Environment.TEST}:
64
+ Env.read_env(BASE_DIR / ".env")
65
+ # Everything a single-host install writes: database, run files and runner
66
+ # workspaces. On a single host its .env also holds keys made on first start.
67
+ DATA_DIR = Path(env.str(AppEnv.DATA_DIR, default="~/.secondshift")).expanduser()
68
+ if SINGLE_HOST:
69
+ write_generated_env(DATA_DIR)
70
+ Env.read_env(DATA_DIR / ".env")
71
+
72
+
73
+ def sqlalchemy_url(database_url: str) -> str:
74
+ """DBOS uses SQLAlchemy, which needs the psycopg driver named explicitly."""
75
+ for scheme in ("postgres://", "postgresql://"):
76
+ if database_url.startswith(scheme):
77
+ return "postgresql+psycopg://" + database_url.removeprefix(scheme)
78
+ return database_url
79
+
80
+
81
+ def run_limit(
82
+ setting: str, default: int, ceiling: int, minimum: int
83
+ ) -> tuple[int, int]:
84
+ """A run limit's default and the highest value an owner may choose.
85
+
86
+ Read from SECONDSHIFT_<setting> and SECONDSHIFT_<setting>_CEILING, the
87
+ names workspaces.limits.LimitSpec shows.
88
+ """
89
+ name = f"SECONDSHIFT_{setting}"
90
+ value = env.int(name, default=default)
91
+ highest = env.int(f"{name}_CEILING", default=max(ceiling, value))
92
+ if value < minimum:
93
+ raise ImproperlyConfigured(
94
+ f"{name} must be positive" if minimum else f"{name} cannot be negative"
95
+ )
96
+ if highest < value:
97
+ raise ImproperlyConfigured(f"{name}_CEILING cannot be below {name}")
98
+ return value, highest
99
+
100
+
101
+ # --- Core --------------------------------------------------------------------
102
+
103
+ # Required in production. Generated into DATA_DIR/.env on a single host.
104
+ SECRET_KEY = env.str(AppEnv.SECRET_KEY, default="")
105
+ FIELD_ENCRYPTION_KEY = env.str(AppEnv.ENCRYPTION_KEY, default="")
106
+ # Off unless asked for, on by default in development. production.py refuses it.
107
+ DEBUG = env.bool(AppEnv.DEBUG, default=DEV)
108
+ FRONTEND_URL = env.str(AppEnv.PUBLIC_URL, default="http://localhost:5173")
109
+ try:
110
+ PUBLIC_HOST = urlsplit(FRONTEND_URL).hostname
111
+ except ValueError:
112
+ PUBLIC_HOST = None
113
+ ALLOWED_HOSTS = env.list(
114
+ AppEnv.ALLOWED_HOSTS,
115
+ default=["localhost", "127.0.0.1"] if SINGLE_HOST else [PUBLIC_HOST or "localhost"],
116
+ )
117
+
118
+ INSTALLED_APPS = [
119
+ "django.contrib.admin",
120
+ "django.contrib.auth",
121
+ "django.contrib.contenttypes",
122
+ "django.contrib.sessions",
123
+ "django.contrib.messages",
124
+ "django.contrib.staticfiles",
125
+ "django.contrib.sites",
126
+ "corsheaders",
127
+ "allauth",
128
+ "allauth.account",
129
+ "allauth.socialaccount",
130
+ "allauth.socialaccount.providers.github",
131
+ "axes",
132
+ "health_check",
133
+ "secondshift.core",
134
+ "secondshift.workspaces",
135
+ "secondshift.connections",
136
+ "secondshift.repositories",
137
+ "secondshift.jobs",
138
+ "secondshift.runs",
139
+ "secondshift.triggers",
140
+ "secondshift.gateway",
141
+ "secondshift.mcp_server",
142
+ ]
143
+
144
+ MIDDLEWARE = [
145
+ "secondshift.core.middleware.LivenessMiddleware",
146
+ "django.middleware.security.SecurityMiddleware",
147
+ "django.middleware.csp.ContentSecurityPolicyMiddleware",
148
+ "whitenoise.middleware.WhiteNoiseMiddleware",
149
+ "corsheaders.middleware.CorsMiddleware",
150
+ "django.contrib.sessions.middleware.SessionMiddleware",
151
+ "django.middleware.common.CommonMiddleware",
152
+ "django.middleware.csrf.CsrfViewMiddleware",
153
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
154
+ "allauth.account.middleware.AccountMiddleware",
155
+ "django.contrib.messages.middleware.MessageMiddleware",
156
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
157
+ "axes.middleware.AxesMiddleware",
158
+ ]
159
+
160
+ # Set by secondshift.config.app. The gateway serves model routes and run-scoped
161
+ # agent tools to trusted orchestrators, never the offline command sandbox.
162
+ SERVICE = Service(env.str(SERVICE_VAR, default=Service.WEB))
163
+ ROOT_URLCONF = (
164
+ "secondshift.gateway.urls"
165
+ if SERVICE == Service.GATEWAY
166
+ else "secondshift.config.urls"
167
+ )
168
+
169
+ TEMPLATES = [
170
+ {
171
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
172
+ "DIRS": [PACKAGE_DIR / "templates"],
173
+ "APP_DIRS": True,
174
+ "OPTIONS": {
175
+ "context_processors": [
176
+ "django.template.context_processors.request",
177
+ "django.contrib.auth.context_processors.auth",
178
+ "django.contrib.messages.context_processors.messages",
179
+ "secondshift.workspaces.context_processors.invite",
180
+ ]
181
+ },
182
+ }
183
+ ]
184
+
185
+ LANGUAGE_CODE = "en-us"
186
+ TIME_ZONE = "UTC"
187
+ USE_I18N = True
188
+ USE_TZ = True
189
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
190
+ SITE_ID = 1
191
+
192
+ LOGGING = logging_config(env.str(AppEnv.LOG_LEVEL, default="INFO").upper(), SERVICE)
193
+
194
+ # --- Database and durable workflows ------------------------------------------
195
+
196
+ DATABASE_URL = env.str(
197
+ AppEnv.DATABASE_URL, default=f"sqlite:///{DATA_DIR / 'secondshift.sqlite3'}"
198
+ )
199
+ DATABASES = {
200
+ "default": {
201
+ **env.db_url_config(DATABASE_URL),
202
+ "CONN_MAX_AGE": env.int(AppEnv.DB_CONN_MAX_AGE_SECONDS, default=60),
203
+ "CONN_HEALTH_CHECKS": True,
204
+ }
205
+ }
206
+ DEFAULT_WORKFLOW_DATABASE_URL = DATABASE_URL
207
+ if DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
208
+ # Dispatch holds an application write lock while DBOS writes through a
209
+ # separate connection. SQLite needs separate files to avoid self-deadlock.
210
+ DEFAULT_WORKFLOW_DATABASE_URL = f"sqlite:///{DATA_DIR / 'workflows.sqlite3'}"
211
+ # Web, gateway and worker share one file. WAL lets reads run during a
212
+ # write, IMMEDIATE transactions and a timeout make writers wait their turn
213
+ # instead of failing with "database is locked".
214
+ DATABASES["default"]["OPTIONS"] = {
215
+ "init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
216
+ "transaction_mode": "IMMEDIATE",
217
+ "timeout": 20,
218
+ }
219
+ # Sign-in rate limits and the health check use the cache. Every process must
220
+ # see the same counts, so the default is a table in the application database.
221
+ CACHES = {
222
+ "default": env.cache_url(AppEnv.CACHE_URL, default="dbcache://secondshift_cache")
223
+ }
224
+
225
+ DBOS_SYSTEM_DATABASE_URL = sqlalchemy_url(
226
+ env.str(
227
+ AppEnv.WORKFLOW_DATABASE_URL,
228
+ default=DEFAULT_WORKFLOW_DATABASE_URL,
229
+ )
230
+ )
231
+ DBOS_APPLICATION_NAME = "secondshift"
232
+ # Fixed, so a deploy still recovers workflows started by the previous version.
233
+ DBOS_APPLICATION_VERSION = "0.1.0"
234
+ DBOS_QUEUE_NAME = "agent-runs"
235
+ DBOS_WORKER_CONCURRENCY = env.int(AppEnv.RUN_CONCURRENCY, default=2)
236
+ DBOS_GLOBAL_CONCURRENCY = env.int(AppEnv.RUN_GLOBAL_CONCURRENCY, default=0)
237
+ TASK_QUEUE_LIMIT = env.int(AppEnv.TASK_QUEUE_LIMIT, default=100)
238
+
239
+ # --- Static files and run artifacts ------------------------------------------
240
+
241
+ STATIC_URL = "/static/"
242
+ STATIC_ROOT = BASE_DIR / "staticfiles"
243
+ WHITENOISE_IMMUTABLE_FILE_TEST = immutable_static_file
244
+ # The built dashboard. In a checkout also its source assets, so admin has its
245
+ # icon before the first build.
246
+ STATICFILES_DIRS = [
247
+ path for path in (WEB_DIR, BASE_DIR / "dashboard" / "public") if path.is_dir()
248
+ ]
249
+
250
+ # Private run output, never served over HTTP. "filesystem" needs a persistent
251
+ # volume at SECONDSHIFT_ARTIFACT_ROOT in production, "s3" works with any
252
+ # S3-compatible service (AWS, R2, Hetzner, RustFS locally).
253
+ ARTIFACT_STORAGE = env.str(AppEnv.ARTIFACT_STORAGE, default=ArtifactStorage.FILESYSTEM)
254
+ ARTIFACT_STORAGES = {
255
+ ArtifactStorage.FILESYSTEM: {
256
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
257
+ "OPTIONS": {
258
+ "location": env.path(
259
+ "SECONDSHIFT_ARTIFACT_ROOT", default=DATA_DIR / "artifacts"
260
+ ),
261
+ "base_url": None,
262
+ },
263
+ },
264
+ ArtifactStorage.S3: {
265
+ "BACKEND": "storages.backends.s3.S3Storage",
266
+ "OPTIONS": {
267
+ "bucket_name": env.str("SECONDSHIFT_ARTIFACT_BUCKET", default=""),
268
+ "endpoint_url": env.str("SECONDSHIFT_ARTIFACT_ENDPOINT_URL", default=None),
269
+ "region_name": env.str("SECONDSHIFT_ARTIFACT_REGION", default=None),
270
+ # Unset keys fall back to boto3's credential chain (IAM roles etc.).
271
+ "access_key": env.str("SECONDSHIFT_ARTIFACT_ACCESS_KEY_ID", default=None),
272
+ "secret_key": env.str(
273
+ "SECONDSHIFT_ARTIFACT_SECRET_ACCESS_KEY", default=None
274
+ ),
275
+ "location": env.str("SECONDSHIFT_ARTIFACT_PREFIX", default=""),
276
+ "default_acl": "private",
277
+ "querystring_auth": True,
278
+ "file_overwrite": True,
279
+ "addressing_style": env.str(
280
+ "SECONDSHIFT_ARTIFACT_ADDRESSING_STYLE", default=None
281
+ ),
282
+ },
283
+ },
284
+ }
285
+ if ARTIFACT_STORAGE not in ARTIFACT_STORAGES:
286
+ raise ImproperlyConfigured(
287
+ f"SECONDSHIFT_ARTIFACT_STORAGE must be one of {sorted(ARTIFACT_STORAGES)}"
288
+ )
289
+ STORAGES = {
290
+ "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
291
+ "run_artifacts": ARTIFACT_STORAGES[ARTIFACT_STORAGE],
292
+ "staticfiles": {"BACKEND": "secondshift.config.static_storage.StaticFilesStorage"},
293
+ }
294
+
295
+ # --- Accounts and login ------------------------------------------------------
296
+
297
+ AUTH_PASSWORD_VALIDATORS = [
298
+ {
299
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
300
+ },
301
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
302
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
303
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
304
+ ]
305
+ AUTHENTICATION_BACKENDS = [
306
+ "axes.backends.AxesStandaloneBackend",
307
+ "django.contrib.auth.backends.ModelBackend",
308
+ "allauth.account.auth_backends.AuthenticationBackend",
309
+ ]
310
+
311
+ # Email goes out only through EMAIL_URL, e.g. smtp://user:pass@host:587.
312
+ EMAIL_URL = env.str(AppEnv.EMAIL_URL, default="")
313
+ DEFAULT_FROM_EMAIL = env.str(AppEnv.FROM_EMAIL, default="webmaster@localhost")
314
+ if EMAIL_URL:
315
+ globals().update(env.email_url(AppEnv.EMAIL_URL))
316
+ elif SINGLE_HOST:
317
+ # One person at the terminal reads them there.
318
+ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
319
+ else:
320
+ # Nowhere to send them. They are dropped so reset links never reach logs.
321
+ EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
322
+ # Whether an email reaches anyone. Password resets are offered only then.
323
+ EMAIL_DELIVERED = bool(EMAIL_URL) or SINGLE_HOST
324
+ # Ask people to confirm their address only when an email can reach them.
325
+ ACCOUNT_EMAIL_VERIFICATION = "optional" if EMAIL_URL or DEV else "none"
326
+
327
+ ACCOUNT_LOGIN_METHODS = {"email"}
328
+ ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
329
+ ACCOUNT_ADAPTER = "secondshift.core.adapters.AccountAdapter"
330
+ SOCIALACCOUNT_ADAPTER = "secondshift.core.adapters.SocialAccountAdapter"
331
+ # New accounts (email or GitHub) are refused unless enabled, open in dev.
332
+ SIGNUP_ENABLED = env.bool("SECONDSHIFT_SIGNUP_ENABLED", default=DEV)
333
+ LOGIN_REDIRECT_URL = "/"
334
+ LOGOUT_REDIRECT_URL = "/"
335
+ GITHUB_CLIENT_ID = env.str("GITHUB_CLIENT_ID", default="")
336
+ GITHUB_CLIENT_SECRET = env.str("GITHUB_CLIENT_SECRET", default="")
337
+ SOCIALACCOUNT_PROVIDERS = {"github": {"SCOPE": ["read:user", "user:email"]}}
338
+ # Without an app, allauth hides the GitHub button on the sign-in page.
339
+ if GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET:
340
+ SOCIALACCOUNT_PROVIDERS["github"]["APP"] = {
341
+ "client_id": GITHUB_CLIENT_ID,
342
+ "secret": GITHUB_CLIENT_SECRET,
343
+ "key": "",
344
+ }
345
+
346
+ # Lock out an IP after repeated failed logins (admin and allauth alike).
347
+ AXES_FAILURE_LIMIT = env.int("AXES_FAILURE_LIMIT", default=5)
348
+ AXES_COOLOFF_TIME = timedelta(minutes=env.int("AXES_COOLOFF_MINUTES", default=30))
349
+ AXES_LOCKOUT_PARAMETERS = ["ip_address"]
350
+ AXES_RESET_ON_SUCCESS = True
351
+ AXES_VERBOSE = False
352
+ # Number of trusted reverse proxies that append to X-Forwarded-For (1 behind
353
+ # kamal-proxy). With 0 the header is ignored so clients cannot spoof their IP.
354
+ AXES_IPWARE_PROXY_COUNT = env.int("SECONDSHIFT_TRUSTED_PROXY_COUNT", default=0) or None
355
+ AXES_IPWARE_META_PRECEDENCE_ORDER = (
356
+ ("HTTP_X_FORWARDED_FOR", "REMOTE_ADDR")
357
+ if AXES_IPWARE_PROXY_COUNT
358
+ else ("REMOTE_ADDR",)
359
+ )
360
+
361
+ # --- Browser security --------------------------------------------------------
362
+
363
+ CSRF_TRUSTED_ORIGINS = env.list(
364
+ AppEnv.CSRF_TRUSTED_ORIGINS, default=[FRONTEND_URL.rstrip("/")]
365
+ )
366
+ X_FRAME_OPTIONS = "DENY"
367
+ # PrimeVue injects component styles at runtime, so styles need 'unsafe-inline'.
368
+ # Scripts stay restricted to same-origin files.
369
+ SECURE_CSP = {
370
+ "default-src": [CSP.SELF],
371
+ "script-src": [CSP.SELF],
372
+ "style-src": [CSP.SELF, CSP.UNSAFE_INLINE, "https://fonts.googleapis.com"],
373
+ "font-src": [CSP.SELF, "https://fonts.gstatic.com"],
374
+ "img-src": [CSP.SELF, "data:"],
375
+ "connect-src": [CSP.SELF],
376
+ "form-action": [CSP.SELF, GITHUB_BASE_URL],
377
+ "frame-ancestors": [CSP.NONE],
378
+ "base-uri": [CSP.SELF],
379
+ "object-src": [CSP.NONE],
380
+ }
381
+
382
+ # --- Repositories and runner -------------------------------------------------
383
+
384
+ # Reading repositories from this machine's filesystem is a development feature.
385
+ LOCAL_REPOSITORIES_ENABLED = SINGLE_HOST and env.bool(
386
+ "SECONDSHIFT_LOCAL_REPOSITORIES_ENABLED", default=True
387
+ )
388
+ # The control plane's copies of repository sources: one Git mirror per
389
+ # repository and the snapshots runs get. Both are caches. Deleting them only
390
+ # costs a fetch from the source again.
391
+ SOURCE_CACHE_ROOT = Path(
392
+ env.str(AppEnv.SOURCE_CACHE_ROOT, default=str(DATA_DIR / "sources"))
393
+ ).resolve()
394
+ # Tasks and jobs may run skills from other public GitHub repositories.
395
+ EXTERNAL_SKILLS_ENABLED = env.bool(AppEnv.EXTERNAL_SKILLS_ENABLED, default=True)
396
+ # Where skills from other repositories are fetched from. Tests point it at a
397
+ # local folder. Only public repositories are read, without credentials.
398
+ EXTERNAL_SKILLS_BASE_URL = GITHUB_BASE_URL
399
+ # Snapshots past this size are removed, oldest first.
400
+ SOURCE_CACHE_MAX_BYTES = env.int(
401
+ AppEnv.SOURCE_CACHE_MAX_BYTES, default=2 * 1024 * 1024 * 1024
402
+ )
403
+ # Run limits. These are the defaults. A workspace owner can choose other values
404
+ # for the workspace or a job, up to the *_CEILING maximums. Repository manifests
405
+ # cannot set them. The values that apply are frozen into each execution plan.
406
+ # The ceilings are the highest limits a workspace owner can choose for a
407
+ # workspace or a job. The defaults suit short reviews. The ceilings leave room
408
+ # for long jobs, such as an audit, so an owner can raise one job's limits
409
+ # without a server change.
410
+ AGENT_MAX_TURNS, AGENT_MAX_TURNS_CEILING = run_limit(
411
+ "AGENT_MAX_TURNS", default=24, ceiling=200, minimum=1
412
+ )
413
+ AGENT_MAX_TOOL_CALLS, AGENT_MAX_TOOL_CALLS_CEILING = run_limit(
414
+ "AGENT_MAX_TOOL_CALLS", default=150, ceiling=500, minimum=0
415
+ )
416
+ AGENT_MAX_TOOL_RESULT_CHARS = env.int(
417
+ "SECONDSHIFT_AGENT_MAX_TOOL_RESULT_CHARS", default=65_536
418
+ )
419
+ if AGENT_MAX_TOOL_RESULT_CHARS < 1:
420
+ raise ImproperlyConfigured(
421
+ "SECONDSHIFT_AGENT_MAX_TOOL_RESULT_CHARS must be positive"
422
+ )
423
+ RUNNER_RUNTIME = env.str(AppEnv.RUNNER_RUNTIME, default=SandboxRuntime.DOCKER)
424
+ RUNNER_IMAGE = env.str(AppEnv.RUNNER_IMAGE, default=DEFAULT_SANDBOX_IMAGE)
425
+ # The orchestrator receives only this runner-scoped credential. It never receives
426
+ # the runtime socket or runner database credentials.
427
+ # On a single host the runner and gateway listen next to the web app.
428
+ RUNNER_SERVICE_URL = env.str(
429
+ "SECONDSHIFT_RUNNER_SERVICE_URL",
430
+ default="http://127.0.0.1:8002" if SINGLE_HOST else "",
431
+ )
432
+ RUNNER_SERVICE_TOKEN = env.str(AppEnv.RUNNER_SERVICE_TOKEN, default="")
433
+ # The trusted agent orchestrator calls this internal gateway. Command sandboxes
434
+ # always use --network=none and never receive its run-scoped token.
435
+ MODEL_GATEWAY_URL = env.str(
436
+ "SECONDSHIFT_MODEL_GATEWAY_URL",
437
+ default=LOCAL_GATEWAY_URL if SINGLE_HOST else "",
438
+ )
439
+ RUN_TIMEOUT_SECONDS, RUN_TIMEOUT_SECONDS_CEILING = run_limit(
440
+ "RUN_TIMEOUT_SECONDS", default=900, ceiling=7200, minimum=1
441
+ )
442
+ RUN_OUTPUT_MAX_BYTES = env.int(AppEnv.RUN_OUTPUT_MAX_BYTES, default=5 * 1024 * 1024)
443
+ RUN_OUTPUT_EXCERPT_BYTES = 16 * 1024
444
+ # The most earlier reports a job can ask for with `history` in its manifest.
445
+ JOB_HISTORY_MAX = env.int("SECONDSHIFT_JOB_HISTORY_MAX", default=5)
446
+ if JOB_HISTORY_MAX < 0:
447
+ raise ImproperlyConfigured("SECONDSHIFT_JOB_HISTORY_MAX cannot be negative")
448
+ # The most sub-agents one run may start, whatever its manifest asks for.
449
+ RUN_SUBAGENTS_MAX = env.int("SECONDSHIFT_RUN_SUBAGENTS_MAX", default=5)
450
+ if RUN_SUBAGENTS_MAX < 0:
451
+ raise ImproperlyConfigured("SECONDSHIFT_RUN_SUBAGENTS_MAX cannot be negative")
452
+ # The longest an API token can last. Its creator can choose a shorter time.
453
+ API_TOKEN_MAX_DAYS = env.int("SECONDSHIFT_API_TOKEN_MAX_DAYS", default=90)
454
+ if API_TOKEN_MAX_DAYS < 1:
455
+ raise ImproperlyConfigured("SECONDSHIFT_API_TOKEN_MAX_DAYS must be positive")
456
+ # Requests to /mcp each person can make per minute, across all their API tokens.
457
+ MCP_RATE_LIMIT = env.int("SECONDSHIFT_MCP_RATE_LIMIT", default=300)
458
+ if MCP_RATE_LIMIT < 1:
459
+ raise ImproperlyConfigured("SECONDSHIFT_MCP_RATE_LIMIT must be positive")
460
+ # Total text a run starts with from its job's earlier reports, newest first.
461
+ RUN_HISTORY_MAX_CHARS = env.int("SECONDSHIFT_RUN_HISTORY_MAX_CHARS", default=65_536)
462
+ if RUN_HISTORY_MAX_CHARS < 1:
463
+ raise ImproperlyConfigured("SECONDSHIFT_RUN_HISTORY_MAX_CHARS must be positive")
464
+ # Finished runs, their output and DBOS records are deleted after this. 0 keeps them.
465
+ RUN_RETENTION_DAYS = env.int(AppEnv.RUN_RETENTION_DAYS, default=90)
466
+ # How often the worker syncs local repositories whose branch has new commits.
467
+ # 0 turns automatic sync off.
468
+ REPOSITORY_SYNC_INTERVAL_SECONDS = env.int(
469
+ AppEnv.REPOSITORY_SYNC_INTERVAL_SECONDS, default=300
470
+ )
471
+
472
+ # --- Outbound calls and model gateway ----------------------------------------
473
+
474
+ # Webhooks and model providers may only reach public HTTPS endpoints unless
475
+ # local targets are allowed (HTTP, localhost, private networks): dev only.
476
+ OUTBOUND_ALLOW_LOCAL = SINGLE_HOST and env.bool(
477
+ "SECONDSHIFT_OUTBOUND_ALLOW_LOCAL", default=True
478
+ )
479
+ OUTBOUND_TIMEOUT_SECONDS = 10
480
+ # Inbound job triggers: the largest request body accepted, how old a signed
481
+ # timestamp may be, and how many received requests each trigger keeps.
482
+ TRIGGER_MAX_BODY_BYTES = 1024 * 1024
483
+ TRIGGER_SIGNATURE_MAX_AGE_SECONDS = 300
484
+ TRIGGER_EVENTS_KEPT = 100
485
+ # A run keeps this much of the body that started it. The rest is cut.
486
+ TRIGGER_PAYLOAD_MAX_BYTES = 64 * 1024
487
+ # Runs one trigger may start per hour. An owner can change it per trigger, up
488
+ # to the ceiling.
489
+ TRIGGER_MAX_RUNS_PER_HOUR = env.int("SECONDSHIFT_TRIGGER_MAX_RUNS_PER_HOUR", default=10)
490
+ TRIGGER_MAX_RUNS_PER_HOUR_CEILING = env.int(
491
+ "SECONDSHIFT_TRIGGER_MAX_RUNS_PER_HOUR_CEILING", default=60
492
+ )
493
+ # Per-run token budget (input + output + cache tokens), largest accepted
494
+ # request body, and how long to wait between streamed chunks.
495
+ MODEL_RUN_TOKEN_BUDGET, MODEL_RUN_TOKEN_BUDGET_CEILING = run_limit(
496
+ "MODEL_RUN_TOKEN_BUDGET", default=2_000_000, ceiling=20_000_000, minimum=1
497
+ )
498
+ GATEWAY_MAX_BODY_BYTES = 32 * 1024 * 1024
499
+ GATEWAY_READ_TIMEOUT_SECONDS = 600
500
+
501
+ # GitHub App credentials belong only to the control plane, never to sandboxes.
502
+ GITHUB_APP_ID = env.str("SECONDSHIFT_GITHUB_APP_ID", default="")
503
+ GITHUB_APP_SLUG = env.str("SECONDSHIFT_GITHUB_APP_SLUG", default="")
504
+ GITHUB_APP_CLIENT_ID = env.str("SECONDSHIFT_GITHUB_APP_CLIENT_ID", default="")
505
+ GITHUB_APP_CLIENT_SECRET = env.str("SECONDSHIFT_GITHUB_APP_CLIENT_SECRET", default="")
506
+ GITHUB_APP_PRIVATE_KEY = env.str(
507
+ "SECONDSHIFT_GITHUB_APP_PRIVATE_KEY", default=""
508
+ ).replace("\\n", "\n")
509
+
510
+
511
+ # CLI agent execution. Servers turn it on by setting an image pinned by digest.
512
+ # `secondshift up` builds the local tag, which its runner resolves per run.
513
+ AGENT_IMAGE = env.str(
514
+ AppEnv.AGENT_IMAGE,
515
+ default=LOCAL_AGENT_TAG if ENVIRONMENT == Environment.LOCAL else "",
516
+ )
517
+ AGENT_NETWORK = env.str(AppEnv.AGENT_NETWORK, default="bridge")
518
+ AGENT_BROKER_URL = env.str(
519
+ AppEnv.AGENT_BROKER_URL,
520
+ default=local_agent_broker_url(MODEL_GATEWAY_URL) if SINGLE_HOST else "",
521
+ )
522
+ # Agent containers call the gateway by the broker's host name.
523
+ if AGENT_IMAGE and (agent_broker_host := urlsplit(AGENT_BROKER_URL).hostname):
524
+ ALLOWED_HOSTS = list(dict.fromkeys([*ALLOWED_HOSTS, agent_broker_host]))
@@ -0,0 +1,14 @@
1
+ # ruff: noqa: F403, F405
2
+ from secondshift.config.settings.base import *
3
+
4
+ if not DEV:
5
+ raise ImproperlyConfigured(
6
+ "secondshift.config.settings.dev requires SECONDSHIFT_ENV=dev"
7
+ )
8
+
9
+ INSTALLED_APPS += ["django_extensions"]
10
+ # The Vite dev server calls the API from another origin. Its proxy rewrites
11
+ # Host to the backend, so the browser Origin must also be trusted for CSRF.
12
+ CORS_ALLOWED_ORIGINS = [FRONTEND_URL]
13
+ CORS_ALLOW_CREDENTIALS = True
14
+ CSRF_TRUSTED_ORIGINS = [*CSRF_TRUSTED_ORIGINS, FRONTEND_URL]
@@ -0,0 +1,23 @@
1
+ # ruff: noqa: F403, F405
2
+ """`secondshift up`: one person running SecondShift on their own machine."""
3
+
4
+ from secondshift.config.constants import Environment
5
+ from secondshift.config.settings.base import *
6
+
7
+ if ENVIRONMENT != Environment.LOCAL:
8
+ raise ImproperlyConfigured(
9
+ "secondshift.config.settings.local requires SECONDSHIFT_ENV=local"
10
+ )
11
+
12
+ # `secondshift up` serves the dashboard here unless told otherwise. It sets
13
+ # FRONTEND_URL for its processes when --host or --port change it.
14
+ FRONTEND_URL = env.str(AppEnv.PUBLIC_URL, default="http://127.0.0.1:8000")
15
+
16
+ # Static files and the dashboard are served straight from the installed
17
+ # package, so there is no collectstatic step.
18
+ STATIC_ROOT = None
19
+ WHITENOISE_USE_FINDERS = True
20
+ STORAGES = {
21
+ **STORAGES,
22
+ "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
23
+ }
@@ -0,0 +1,113 @@
1
+ # ruff: noqa: F403, F405
2
+ from secondshift.config.constants import (
3
+ AppEnv,
4
+ ArtifactStorage,
5
+ Environment,
6
+ SandboxRuntime,
7
+ Service,
8
+ )
9
+ from secondshift.config.settings.base import *
10
+
11
+ if ENVIRONMENT != Environment.PRODUCTION:
12
+ raise ImproperlyConfigured(
13
+ "secondshift.config.settings.production requires SECONDSHIFT_ENV=production"
14
+ )
15
+
16
+ # Every problem is reported at once, and the process refuses to start.
17
+ errors: list[str] = []
18
+
19
+
20
+ def required(name: str) -> str:
21
+ value = env.str(name, default="")
22
+ if not value:
23
+ errors.append(f"{name} must be set")
24
+ return value
25
+
26
+
27
+ # No development fallbacks for secrets or the database in production.
28
+ SECRET_KEY = required(AppEnv.SECRET_KEY)
29
+ FIELD_ENCRYPTION_KEY = required(AppEnv.ENCRYPTION_KEY)
30
+ required(AppEnv.DATABASE_URL)
31
+ if DEBUG:
32
+ errors.append(f"{AppEnv.DEBUG} must be off")
33
+ if CACHES["default"]["BACKEND"] in {
34
+ "django.core.cache.backends.locmem.LocMemCache",
35
+ "django.core.cache.backends.dummy.DummyCache",
36
+ }:
37
+ errors.append(f"{AppEnv.CACHE_URL} must be shared by every process")
38
+ if SERVICE == Service.WEB and not CSRF_TRUSTED_ORIGINS:
39
+ errors.append(f"{AppEnv.CSRF_TRUSTED_ORIGINS} must be set")
40
+ if SERVICE in {Service.WEB, Service.WORKER}:
41
+ try:
42
+ public_origin = urlsplit(FRONTEND_URL)
43
+ valid_origin = (
44
+ public_origin.scheme == "https"
45
+ and bool(public_origin.hostname)
46
+ and not public_origin.username
47
+ and not public_origin.password
48
+ and public_origin.path in {"", "/"}
49
+ and not public_origin.query
50
+ and not public_origin.fragment
51
+ and public_origin.port != 0
52
+ )
53
+ except ValueError:
54
+ valid_origin = False
55
+ if not valid_origin:
56
+ errors.append(f"{AppEnv.PUBLIC_URL} must be the public HTTPS address")
57
+ if (
58
+ ARTIFACT_STORAGE == ArtifactStorage.S3
59
+ and not ARTIFACT_STORAGES[ArtifactStorage.S3]["OPTIONS"]["bucket_name"]
60
+ ):
61
+ errors.append("SECONDSHIFT_ARTIFACT_BUCKET must be set for s3 storage")
62
+ if RUNNER_RUNTIME != SandboxRuntime.GVISOR:
63
+ errors.append("SECONDSHIFT_RUNNER_RUNTIME must be runsc (gVisor)")
64
+ if TASK_QUEUE_LIMIT < 1:
65
+ errors.append(f"{AppEnv.TASK_QUEUE_LIMIT} must be positive")
66
+ if SERVICE == Service.WORKER:
67
+ if DBOS_WORKER_CONCURRENCY < 1:
68
+ errors.append(f"{AppEnv.RUN_CONCURRENCY} must be positive")
69
+ if DBOS_GLOBAL_CONCURRENCY < 0:
70
+ errors.append(f"{AppEnv.RUN_GLOBAL_CONCURRENCY} must be zero or positive")
71
+ required(AppEnv.RUNNER_SERVICE_URL)
72
+ required(AppEnv.MODEL_GATEWAY_URL)
73
+ if len(required(AppEnv.RUNNER_SERVICE_TOKEN)) < 32:
74
+ errors.append(
75
+ "SECONDSHIFT_RUNNER_SERVICE_TOKEN must contain at least 32 characters"
76
+ )
77
+ # Partial App configuration must not silently disable production repository sync.
78
+ app_configuration = (
79
+ GITHUB_APP_ID,
80
+ GITHUB_APP_SLUG,
81
+ GITHUB_APP_CLIENT_ID,
82
+ GITHUB_APP_CLIENT_SECRET,
83
+ GITHUB_APP_PRIVATE_KEY,
84
+ )
85
+ if SERVICE in {Service.WEB, Service.WORKER} and any(app_configuration):
86
+ if not all(app_configuration):
87
+ errors.append(
88
+ "All SECONDSHIFT_GITHUB_APP_* settings must be configured together"
89
+ )
90
+ if SERVICE == Service.WEB:
91
+ required(AppEnv.RUNNER_SERVICE_URL)
92
+ if len(required(AppEnv.RUNNER_SERVICE_TOKEN)) < 32:
93
+ errors.append(
94
+ "SECONDSHIFT_RUNNER_SERVICE_TOKEN must contain at least 32 characters"
95
+ )
96
+ if errors:
97
+ raise ImproperlyConfigured(
98
+ "Production settings are incomplete:\n- " + "\n- ".join(errors)
99
+ )
100
+
101
+ # --- HTTPS hardening ---------------------------------------------------------
102
+
103
+ # Trust X-Forwarded-Proto only from a TLS-terminating proxy that overwrites it
104
+ # (kamal-proxy does). The app container must not be reachable any other way.
105
+ if env.bool("SECONDSHIFT_BEHIND_TLS_PROXY", default=True):
106
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
107
+ # The gateway process is reached only by trusted orchestration services.
108
+ SECURE_SSL_REDIRECT = SERVICE != Service.GATEWAY
109
+ SESSION_COOKIE_SECURE = True
110
+ CSRF_COOKIE_SECURE = True
111
+ SECURE_HSTS_SECONDS = 31536000
112
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = True
113
+ SECURE_HSTS_PRELOAD = True