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,204 @@
1
+ from datetime import timedelta
2
+
3
+ import djclick as click
4
+ from django.conf import settings
5
+ from django.core.management import call_command
6
+ from django.utils import timezone
7
+
8
+ from secondshift.config.constants import RunStart, SandboxRuntime
9
+ from secondshift.connections.dao import create_connection
10
+ from secondshift.connections.models import Connection
11
+ from secondshift.github.identity import clone_url
12
+ from secondshift.jobs.dao import create_job
13
+ from secondshift.jobs.planning import build_execution_plan
14
+ from secondshift.repositories.dao import create_repository
15
+ from secondshift.runs.dao import add_event, create_run_record, create_task
16
+ from secondshift.runs.models import Run
17
+ from secondshift.workspaces.dao import get_workspace_by_slug
18
+
19
+ DEMO_SKILLS = {
20
+ "weekly-infra": """# Weekly infrastructure review
21
+
22
+ Review infrastructure changes merged in the last seven days.
23
+
24
+ ## Steps
25
+
26
+ 1. List commits under `infra/` and `deploy/` since the last report.
27
+ 2. For each change, note what it affects and who merged it.
28
+ 3. Query the `observability` binding for error rates around each deploy.
29
+ 4. Flag drift between Terraform state and the committed configuration.
30
+
31
+ ## Report
32
+
33
+ - Start with one line: how many changes, how many need attention.
34
+ - List each finding with the commit, the evidence and a suggested owner.
35
+ - Do not suggest changes you cannot point to evidence for.
36
+ """,
37
+ }
38
+
39
+
40
+ @click.command()
41
+ @click.option(
42
+ "--yes",
43
+ is_flag=True,
44
+ help="Confirm destroying every record in the development database.",
45
+ )
46
+ def command(yes: bool) -> None:
47
+ """Reset the development database, migrate it, and load demo data."""
48
+ if not settings.DEV:
49
+ raise click.ClickException("dev_reset_db only runs with SECONDSHIFT_ENV=dev.")
50
+ if not yes:
51
+ raise click.ClickException(
52
+ "dev_reset_db destroys the development database. Pass --yes to continue."
53
+ )
54
+ click.echo("Resetting the development database...")
55
+ call_command("reset_db", interactive=False, close_sessions=True, verbosity=0)
56
+ call_command("migrate", interactive=False, verbosity=0)
57
+ call_command("bootstrap_dev")
58
+ seed_demo_data()
59
+ click.secho("Development demo data is ready.", fg="green")
60
+
61
+
62
+ def seed_demo_data() -> None:
63
+ """Load safe, credential-free records for exploring the development UI."""
64
+ workspace = get_workspace_by_slug("secondshift")
65
+ now = timezone.now()
66
+
67
+ github = create_connection(
68
+ workspace=workspace,
69
+ provider=Connection.Provider.GITHUB,
70
+ kind=Connection.Kind.SOURCE,
71
+ display_name="GitHub · demo",
72
+ external_account_id="demo-github",
73
+ status=Connection.Status.CONNECTED,
74
+ auth_strategy=Connection.AuthStrategy.MANAGED,
75
+ metadata={"demo": True},
76
+ )
77
+
78
+ repositories = {
79
+ name: create_repository(
80
+ workspace=workspace,
81
+ github_connection=github,
82
+ # GitHub repository ids are numbers.
83
+ external_id=str(900_000 + index),
84
+ full_name=name,
85
+ clone_url=clone_url(name),
86
+ default_branch=branch,
87
+ enabled=enabled,
88
+ config_sha=commit_sha,
89
+ last_synced_at=now - timedelta(minutes=index * 7),
90
+ )
91
+ for index, (name, branch, enabled, commit_sha) in enumerate(
92
+ (
93
+ ("wild-edge/wild-edge", "main", True, "8f3c9d2" * 5 + "8f3c9"),
94
+ ("secondshift/examples", "main", True, "34a0be7" * 5 + "34a0b"),
95
+ ("acme/platform-infra", "production", False, "c77b103" * 5 + "c77b1"),
96
+ ),
97
+ start=1,
98
+ )
99
+ }
100
+
101
+ jobs = {
102
+ key: create_job(
103
+ workspace=workspace,
104
+ repository=repositories[repository],
105
+ key=key,
106
+ name=name,
107
+ skill_path=skill_path,
108
+ skill_text=DEMO_SKILLS.get(key, ""),
109
+ cron=cron,
110
+ timezone=job_timezone,
111
+ enabled=False,
112
+ config_hash=(key.encode().hex() + "0" * 64)[:64],
113
+ )
114
+ for key, name, repository, skill_path, cron, job_timezone in (
115
+ (
116
+ "weekly-infra",
117
+ "Weekly infrastructure review",
118
+ "wild-edge/wild-edge",
119
+ ".agents/skills/infrastructure-review/SKILL.md",
120
+ "0 9 * * 1",
121
+ "Europe/Warsaw",
122
+ ),
123
+ (
124
+ "dependency-sweep",
125
+ "Dependency freshness sweep",
126
+ "wild-edge/wild-edge",
127
+ ".agents/skills/dependency-sweep/SKILL.md",
128
+ "30 7 * * 2",
129
+ "UTC",
130
+ ),
131
+ (
132
+ "release-notes",
133
+ "Release notes digest",
134
+ "secondshift/examples",
135
+ ".agents/skills/release-notes/SKILL.md",
136
+ "0 16 * * 5",
137
+ "America/New_York",
138
+ ),
139
+ (
140
+ "incident-followup",
141
+ "Incident follow-up",
142
+ "acme/platform-infra",
143
+ ".agents/skills/incident-review/SKILL.md",
144
+ "0 */6 * * *",
145
+ "UTC",
146
+ ),
147
+ )
148
+ }
149
+
150
+ run_specs = (
151
+ (
152
+ "weekly-infra",
153
+ Run.Status.SUCCEEDED,
154
+ 1,
155
+ "Reviewed 14 infrastructure changes; no critical drift found.",
156
+ 0,
157
+ ),
158
+ (
159
+ "dependency-sweep",
160
+ Run.Status.SUCCEEDED,
161
+ 2,
162
+ "Found two dependency updates ready for review.",
163
+ 0,
164
+ ),
165
+ (
166
+ "weekly-infra",
167
+ Run.Status.FAILED,
168
+ 3,
169
+ "The observability capability needs attention.",
170
+ 1,
171
+ ),
172
+ (
173
+ "release-notes",
174
+ Run.Status.SUCCEEDED,
175
+ 5,
176
+ "Prepared a release digest for the engineering team.",
177
+ 0,
178
+ ),
179
+ )
180
+ for index, (job_key, status, days_ago, summary, exit_code) in enumerate(
181
+ run_specs, start=1
182
+ ):
183
+ finished_at = now - timedelta(days=days_ago)
184
+ run = create_run_record(
185
+ task=create_task(repository=jobs[job_key].repository),
186
+ started_by=RunStart.SCHEDULE,
187
+ execution_plan=build_execution_plan(jobs[job_key]).stored(),
188
+ job=jobs[job_key],
189
+ status=status,
190
+ commit_sha=jobs[job_key].repository.config_sha,
191
+ scheduled_for=finished_at - timedelta(minutes=4),
192
+ started_at=finished_at - timedelta(minutes=4),
193
+ finished_at=finished_at,
194
+ sandbox_runtime=SandboxRuntime.GVISOR,
195
+ exit_code=exit_code,
196
+ summary=summary,
197
+ dbos_workflow_id=f"demo-run-{index}",
198
+ )
199
+ add_event(
200
+ run.pk,
201
+ summary,
202
+ level="info" if status == Run.Status.SUCCEEDED else "error",
203
+ metadata={"demo": True},
204
+ )
@@ -0,0 +1,23 @@
1
+ import djclick as click
2
+ from django.conf import settings
3
+
4
+ from secondshift.core import local_output
5
+ from secondshift.core.signin import SIGN_IN_LINK_SECONDS, sign_in_url
6
+ from secondshift.workspaces.owners import get_owner, has_owner
7
+
8
+
9
+ @click.command()
10
+ @click.option(
11
+ "--url",
12
+ default=None,
13
+ help="Where SecondShift is running. Defaults to where secondshift up serves it.",
14
+ )
15
+ def command(url: str | None) -> None:
16
+ """Print a one-time link that signs you in as the owner."""
17
+ if not has_owner():
18
+ raise click.ClickException("There is no owner yet. Run secondshift up first.")
19
+ state = local_output.load_state()
20
+ base = url or (state["url"] if state else settings.FRONTEND_URL)
21
+ minutes = SIGN_IN_LINK_SECONDS // 60
22
+ click.echo(f"This link works once, within {minutes} minutes:")
23
+ click.echo(sign_in_url(base, get_owner()))
@@ -0,0 +1,58 @@
1
+ """Inspect local service diagnostics."""
2
+
3
+ import time
4
+ from collections import deque
5
+ from pathlib import Path
6
+
7
+ import djclick as click
8
+
9
+ from secondshift.core.local_output import LOG_NAMES, log_directory
10
+
11
+
12
+ @click.command()
13
+ @click.option("--service", type=click.Choice(LOG_NAMES))
14
+ @click.option("--lines", default=50, type=click.IntRange(1, 10000), show_default=True)
15
+ @click.option("--follow", is_flag=True, help="Stream new log entries until Ctrl+C.")
16
+ def command(service: str | None, lines: int, follow: bool) -> None:
17
+ """Show saved local service logs."""
18
+ names = [service] if service else LOG_NAMES
19
+ paths = [log_directory() / f"{name}.log" for name in names]
20
+ positions = {path: print_tail(path, lines) for path in paths if path.exists()}
21
+ if not positions and not follow:
22
+ click.echo("No local logs yet. Run secondshift up first.")
23
+ try:
24
+ while follow:
25
+ for path in paths:
26
+ position = read_new(path, positions.get(path))
27
+ if position is not None:
28
+ positions[path] = position
29
+ time.sleep(0.2)
30
+ except KeyboardInterrupt:
31
+ pass
32
+
33
+
34
+ def print_tail(path: Path, lines: int) -> tuple[int, int]:
35
+ """Print the last lines of a log and return where reading stopped."""
36
+ with path.open(errors="replace") as stream:
37
+ for line in deque(stream, maxlen=lines):
38
+ click.echo(line, nl=False)
39
+ return path.stat().st_ino, stream.tell()
40
+
41
+
42
+ def read_new(path: Path, position: tuple[int, int] | None) -> tuple[int, int] | None:
43
+ """Print lines added since position, starting over if the file rotated.
44
+
45
+ A position is the file's inode and the offset already printed.
46
+ """
47
+ try:
48
+ with path.open(errors="replace") as stream:
49
+ stat = path.stat()
50
+ inode, offset = position or (stat.st_ino, 0)
51
+ if inode != stat.st_ino or stat.st_size < offset:
52
+ offset = 0
53
+ stream.seek(offset)
54
+ for line in stream:
55
+ click.echo(line, nl=False)
56
+ return stat.st_ino, stream.tell()
57
+ except FileNotFoundError:
58
+ return position
@@ -0,0 +1,46 @@
1
+ """Inspect the local supervisor without starting services."""
2
+
3
+ import os
4
+
5
+ import djclick as click
6
+
7
+ from secondshift.core import launcher, local_output, terminal
8
+
9
+
10
+ @click.command()
11
+ def command() -> None:
12
+ """Show local service status and the dashboard address."""
13
+ if not local_output.is_running():
14
+ click.echo("SecondShift is stopped. Start it with secondshift up.")
15
+ return
16
+ state = local_output.load_state()
17
+ if state is None:
18
+ click.echo("SecondShift is starting.")
19
+ return
20
+ click.echo(f"Dashboard: {terminal.link(state['url'])}")
21
+ all_ready = True
22
+ for name, service in state["services"].items():
23
+ ready = is_alive(service["pid"]) and launcher.is_ready(
24
+ name, service["health_url"]
25
+ )
26
+ all_ready &= ready
27
+ label = (
28
+ click.style("ready", fg="green")
29
+ if ready
30
+ else click.style("starting or unavailable", fg="yellow")
31
+ )
32
+ click.echo(f" {name}: {label}")
33
+ if not all_ready:
34
+ terminal.note(local_output.LOGS_HINT)
35
+ raise SystemExit(1)
36
+
37
+
38
+ def is_alive(pid: int) -> bool:
39
+ try:
40
+ os.kill(pid, 0)
41
+ except ProcessLookupError:
42
+ return False
43
+ except PermissionError:
44
+ # The process exists but belongs to another user.
45
+ return True
46
+ return True
@@ -0,0 +1,244 @@
1
+ import ipaddress
2
+ import os
3
+ import socket
4
+ import subprocess
5
+ import sys
6
+ import webbrowser
7
+ from importlib.metadata import version
8
+
9
+ import djclick as click
10
+ from django.conf import settings
11
+ from django.core.management import call_command
12
+
13
+ from secondshift.config.constants import AppEnv, Environment, Service
14
+ from secondshift.core import launcher, local_agents, local_output, terminal
15
+ from secondshift.core.signin import SIGN_IN_LINK_SECONDS, sign_in_url
16
+ from secondshift.workspaces.owners import (
17
+ ensure_owner,
18
+ get_owner,
19
+ has_owner,
20
+ set_owner_credentials,
21
+ )
22
+
23
+
24
+ @click.command()
25
+ @click.option(
26
+ "--host", default=None, help="Web listen host. Overrides SECONDSHIFT_HTTP_BIND."
27
+ )
28
+ @click.option(
29
+ "--port",
30
+ default=None,
31
+ type=int,
32
+ help="Web listen port. Overrides SECONDSHIFT_HTTP_BIND.",
33
+ )
34
+ @click.option(
35
+ "--email",
36
+ envvar="SECONDSHIFT_OWNER_EMAIL",
37
+ help="Owner email. Needed only to share on the network with --host.",
38
+ )
39
+ @click.option(
40
+ "--password",
41
+ envvar="SECONDSHIFT_OWNER_PASSWORD",
42
+ help="Owner password. Needed only to share on the network with --host.",
43
+ )
44
+ @click.option(
45
+ "--browser/--no-browser",
46
+ default=True,
47
+ show_default=True,
48
+ help="Open the sign-in link in your browser once started.",
49
+ )
50
+ @click.option(
51
+ "--agents/--no-agents",
52
+ default=True,
53
+ show_default=True,
54
+ help="Let jobs run through Claude Code or Codex subscriptions.",
55
+ )
56
+ @click.option("--debug", is_flag=True, help="Stream service diagnostics to stderr.")
57
+ def command(
58
+ host: str | None,
59
+ port: int | None,
60
+ email: str | None,
61
+ password: str | None,
62
+ browser: bool,
63
+ agents: bool,
64
+ debug: bool,
65
+ ) -> None:
66
+ """Start SecondShift on this computer."""
67
+ if settings.ENVIRONMENT != Environment.LOCAL:
68
+ raise click.ClickException(
69
+ "secondshift up runs with SECONDSHIFT_ENV=local. "
70
+ "In a checkout, use process-compose instead."
71
+ )
72
+ try:
73
+ with local_output.instance_lock():
74
+ start(host, port, email, password, browser, agents, debug)
75
+ except local_output.AlreadyRunning as error:
76
+ raise click.ClickException(
77
+ "SecondShift is already running or starting. Run secondshift status."
78
+ ) from error
79
+
80
+
81
+ def start(
82
+ host: str | None,
83
+ port: int | None,
84
+ email: str | None,
85
+ password: str | None,
86
+ browser: bool,
87
+ agents: bool,
88
+ debug: bool,
89
+ ) -> None:
90
+ parent = dict(os.environ)
91
+ if debug:
92
+ parent[AppEnv.LOG_LEVEL] = "DEBUG"
93
+ configured_host, configured_port = launcher.web_address("127.0.0.1", 8000, parent)
94
+ host, port = host or configured_host, port if port is not None else configured_port
95
+ parent.pop(AppEnv.HTTP_BIND, None)
96
+ terminal.heading(f"SecondShift {version('secondshift')}")
97
+ check_port(host, port)
98
+ check_service_ports()
99
+ check_docker()
100
+ terminal.done("Docker is ready")
101
+ if not (settings.WEB_DIR / "index.html").exists():
102
+ raise click.ClickException(
103
+ "The dashboard isn't built. In a checkout, run "
104
+ "npm --prefix dashboard run build first."
105
+ )
106
+
107
+ call_command("migrate", interactive=False, verbosity=0)
108
+ terminal.done("Local data is ready", str(settings.DATA_DIR))
109
+ agent_host = None
110
+ if agents:
111
+ try:
112
+ local_agents.prepare_agent_image(settings.AGENT_IMAGE, terminal.note)
113
+ agent_host = local_agents.container_gateway_host()
114
+ terminal.done("Agent image is ready")
115
+ except local_agents.AgentSetupError as error:
116
+ raise click.ClickException(
117
+ f"{error} Run with --no-agents to start without agent subscriptions."
118
+ ) from error
119
+ else:
120
+ # An empty value overrides the local default, in every process.
121
+ parent[AppEnv.AGENT_IMAGE] = ""
122
+ services = launcher.services(host, port, parent, agent_host=agent_host)
123
+ runner = next(service for service in services if service.name == Service.RUNNER)
124
+ prepare_runner(runner, debug)
125
+ prepare_owner(host, email, password)
126
+
127
+ url = launcher.public_url(host, port)
128
+
129
+ def ready() -> None:
130
+ link = sign_in_url(url, get_owner())
131
+ minutes = SIGN_IN_LINK_SECONDS // 60
132
+ terminal.done("All services are ready")
133
+ click.echo()
134
+ click.secho("SecondShift is running at ", fg="green", bold=True, nl=False)
135
+ click.echo(terminal.link(url))
136
+ if browser and webbrowser.open(link):
137
+ click.echo("Opening your dashboard...")
138
+ else:
139
+ click.echo(f"Sign in with this link (valid for {minutes} minutes):")
140
+ click.echo(f" {terminal.link(link)}")
141
+ click.echo()
142
+ terminal.note("Keep this terminal open. Press Ctrl+C to stop.")
143
+ terminal.note("Logs: secondshift logs --follow")
144
+
145
+ terminal.note("Starting the web app, model gateway, runner and worker...")
146
+ code = launcher.run(services, ready, click.echo, debug=debug, url=url)
147
+ if code:
148
+ click.echo(local_output.LOGS_HINT, err=True)
149
+ sys.exit(code)
150
+
151
+
152
+ def is_loopback(host: str) -> bool:
153
+ if host == "localhost":
154
+ return True
155
+ try:
156
+ return ipaddress.ip_address(host).is_loopback
157
+ except ValueError:
158
+ return False
159
+
160
+
161
+ def check_docker() -> None:
162
+ try:
163
+ result = subprocess.run(
164
+ ["docker", "info"], capture_output=True, timeout=15, check=False
165
+ )
166
+ except (FileNotFoundError, subprocess.TimeoutExpired):
167
+ result = None
168
+ if result is None or result.returncode:
169
+ raise click.ClickException(
170
+ "Docker isn't running. Skills run in Docker sandboxes. "
171
+ "Start Docker and try again."
172
+ )
173
+
174
+
175
+ def prepare_runner(runner: launcher.Process, debug: bool) -> None:
176
+ migration = subprocess.Popen(
177
+ [sys.executable, "-m", "secondshift", "migrate", "--verbosity", "0"],
178
+ env=runner.env,
179
+ stdout=subprocess.PIPE,
180
+ stderr=subprocess.STDOUT,
181
+ )
182
+ assert migration.stdout is not None
183
+ reader = local_output.capture(migration.stdout, local_output.SETUP_LOG, debug)
184
+ try:
185
+ code = migration.wait()
186
+ except KeyboardInterrupt:
187
+ migration.terminate()
188
+ migration.wait()
189
+ raise
190
+ finally:
191
+ reader.join(timeout=2)
192
+ if code:
193
+ raise click.ClickException(
194
+ "Couldn't prepare runner data. Run secondshift logs --service setup."
195
+ )
196
+
197
+
198
+ def prepare_owner(host: str, email: str | None, password: str | None) -> None:
199
+ if not has_owner():
200
+ ensure_owner()
201
+ if not is_loopback(host) and not get_owner().has_usable_password():
202
+ # Other machines can reach the app, so a password must guard it.
203
+ click.echo("Sharing on the network needs an owner email and password.")
204
+ set_owner_credentials(
205
+ email or click.prompt("Owner email"),
206
+ password
207
+ or click.prompt(
208
+ "Owner password", hide_input=True, confirmation_prompt=True
209
+ ),
210
+ )
211
+
212
+
213
+ def check_port(
214
+ host: str,
215
+ port: int,
216
+ hint: str = "Try secondshift up --port with an available port.",
217
+ ) -> None:
218
+ try:
219
+ addresses = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
220
+ family, kind, protocol, _, address = addresses[0]
221
+ with socket.socket(family, kind, protocol) as listener:
222
+ # The server binds the same way. Without this, closed connections
223
+ # from the last run keep the port looking busy for a minute.
224
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
225
+ listener.bind(address)
226
+ except OSError as error:
227
+ raise click.ClickException(
228
+ f"Couldn't listen on {host}:{port}: {error.strerror or error}. {hint}"
229
+ ) from error
230
+
231
+
232
+ def check_service_ports() -> None:
233
+ """--port moves only the web app. The gateway and runner keep their URLs."""
234
+ for name, url, variable in (
235
+ ("model gateway", settings.MODEL_GATEWAY_URL, AppEnv.MODEL_GATEWAY_URL),
236
+ ("runner", settings.RUNNER_SERVICE_URL, AppEnv.RUNNER_SERVICE_URL),
237
+ ):
238
+ check_port(
239
+ "127.0.0.1",
240
+ launcher.local_service_port(url),
241
+ f"The {name} needs this port. Another SecondShift may be running. "
242
+ f"To use another port, set {variable}, "
243
+ "for example to http://127.0.0.1:8202.",
244
+ )
@@ -0,0 +1,21 @@
1
+ from collections.abc import Callable
2
+
3
+ from django.http import HttpRequest, HttpResponse
4
+
5
+ LIVENESS_PATH = "/-/health/up/"
6
+
7
+
8
+ class LivenessMiddleware:
9
+ """Answer the proxy's liveness probe before host validation and SSL redirects.
10
+
11
+ Kamal's proxy probes the container directly over HTTP with an internal Host
12
+ header, so this must run ahead of SecurityMiddleware and ALLOWED_HOSTS.
13
+ """
14
+
15
+ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
16
+ self.get_response = get_response
17
+
18
+ def __call__(self, request: HttpRequest) -> HttpResponse:
19
+ if request.path == LIVENESS_PATH:
20
+ return HttpResponse("ok", content_type="text/plain")
21
+ return self.get_response(request)
File without changes
@@ -0,0 +1,71 @@
1
+ from datetime import date, datetime
2
+
3
+ from ninja import Schema
4
+
5
+ from secondshift.connections.schemas import ExecutionStatusSchema
6
+
7
+
8
+ class UserSchema(Schema):
9
+ email: str
10
+ workspace_name: str | None
11
+ workspace_slug: str | None
12
+ # The person's role in the current workspace and what it lets them do.
13
+ role: str | None
14
+ permissions: list[str]
15
+ # Django admin only lets staff in, so links to it are shown only to them.
16
+ can_use_admin: bool
17
+ can_create_workspace: bool
18
+ local_repositories_enabled: bool
19
+ # The operator set up the GitHub App, so repositories can come from GitHub.
20
+ github_app_configured: bool = False
21
+ environment: str = "Production"
22
+ sandbox: str = "gVisor sandbox"
23
+
24
+
25
+ class FailingJobSchema(Schema):
26
+ job_id: int
27
+ job: str
28
+ repository: str
29
+ run_id: int
30
+ failed_at: datetime | None
31
+ label: str
32
+ reason: str
33
+ paused: bool = False
34
+
35
+
36
+ class JobSpendSchema(Schema):
37
+ job_id: int
38
+ job: str
39
+ repository: str
40
+ runs: int
41
+ spend_usd: float
42
+ partial: bool
43
+
44
+
45
+ class RunDaySchema(Schema):
46
+ day: date
47
+ succeeded: int
48
+ failed: int
49
+ canceled: int
50
+
51
+
52
+ class OverviewSchema(Schema):
53
+ repositories: int
54
+ local_repositories: int
55
+ enabled_jobs: int
56
+ runs_today: int
57
+ failed_runs: int
58
+ connected_services: int
59
+ failing_jobs: list[FailingJobSchema] = []
60
+ runs_7d: int = 0
61
+ runs_previous_7d: int = 0
62
+ finished_7d: int = 0
63
+ succeeded_7d: int = 0
64
+ spend_7d_usd: float = 0
65
+ spend_previous_7d_usd: float = 0
66
+ spend_partial: bool = False
67
+ spend_by_job: list[JobSpendSchema] = []
68
+ run_days: list[RunDaySchema] = []
69
+ execution: ExecutionStatusSchema | None = None
70
+ # Whether this person has a usable MCP token in this workspace.
71
+ has_agent_token: bool = False