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,94 @@
1
+ from django.conf import settings
2
+ from django.contrib.auth.models import AbstractBaseUser
3
+ from django.db import transaction
4
+ from django.utils.text import slugify
5
+
6
+ from secondshift.workspaces.access import ROLE_RANK, Role
7
+ from secondshift.workspaces.dao import (
8
+ create_workspace,
9
+ lock_other_owners_exist,
10
+ owned_workspace_count,
11
+ slug_taken,
12
+ workspace_members,
13
+ )
14
+ from secondshift.workspaces.models import Workspace, WorkspaceMembership
15
+
16
+ LAST_OWNER_MESSAGE = "A workspace needs at least one owner."
17
+ # How many workspaces someone who is not staff may own, so open sign-up
18
+ # cannot be used to fill the database.
19
+ OWNED_WORKSPACE_LIMIT = 3
20
+
21
+
22
+ class MembershipError(Exception):
23
+ pass
24
+
25
+
26
+ class WorkspaceCreationError(Exception):
27
+ pass
28
+
29
+
30
+ class WorkspaceCreationRefused(WorkspaceCreationError):
31
+ """The person may not create one, as opposed to asking for a bad one."""
32
+
33
+
34
+ def can_create_workspace(user: AbstractBaseUser) -> bool:
35
+ """Anyone may when sign-up is open. Otherwise only staff, who run the server."""
36
+ return bool(settings.SIGNUP_ENABLED or getattr(user, "is_staff", False))
37
+
38
+
39
+ def free_slug(name: str) -> str:
40
+ """A slug from the name, with a number added when it is taken."""
41
+ base = slugify(name)[:40].strip("-") or "workspace"
42
+ slug, number = base, 1
43
+ while slug_taken(slug):
44
+ number += 1
45
+ slug = f"{base}-{number}"
46
+ return slug
47
+
48
+
49
+ def create_workspace_for(user: AbstractBaseUser, name: str) -> Workspace:
50
+ """A workspace someone made for themselves from the dashboard."""
51
+ if not can_create_workspace(user):
52
+ raise WorkspaceCreationRefused(
53
+ "Only server staff can create workspaces. Ask an owner for an invite."
54
+ )
55
+ name = name.strip()
56
+ if not name:
57
+ raise WorkspaceCreationError("Give the workspace a name.")
58
+ owned = owned_workspace_count(user)
59
+ if not getattr(user, "is_staff", False) and owned >= OWNED_WORKSPACE_LIMIT:
60
+ raise WorkspaceCreationRefused(
61
+ f"You can own up to {OWNED_WORKSPACE_LIMIT} workspaces."
62
+ )
63
+ return create_workspace(name, free_slug(name), user)
64
+
65
+
66
+ def members_in_order(workspace: Workspace) -> list[WorkspaceMembership]:
67
+ """Owners first, then admins, then members, each in the order they joined."""
68
+ return sorted(
69
+ workspace_members(workspace),
70
+ key=lambda membership: -ROLE_RANK[Role(membership.role)],
71
+ )
72
+
73
+
74
+ def ensure_another_owner(membership: WorkspaceMembership) -> None:
75
+ """Refuse to take away the workspace's last owner."""
76
+ if membership.role != Role.OWNER:
77
+ return
78
+ if not lock_other_owners_exist(membership):
79
+ raise MembershipError(LAST_OWNER_MESSAGE)
80
+
81
+
82
+ @transaction.atomic
83
+ def change_role(membership: WorkspaceMembership, role: str) -> WorkspaceMembership:
84
+ if Role(role) != Role.OWNER:
85
+ ensure_another_owner(membership)
86
+ membership.role = Role(role)
87
+ membership.save(update_fields=["role"])
88
+ return membership
89
+
90
+
91
+ @transaction.atomic
92
+ def remove_member(membership: WorkspaceMembership) -> None:
93
+ ensure_another_owner(membership)
94
+ membership.delete()
@@ -0,0 +1,108 @@
1
+ """API tokens: how MCP clients sign in as a person in one workspace.
2
+
3
+ A token is shown once, when it is created. The database keeps a hash of it.
4
+ A token works until it expires or is revoked, and only while its creator
5
+ still belongs to its workspace. Its scope limits what it can change. See
6
+ TOKEN_ACTIONS in secondshift.workspaces.access.
7
+ """
8
+
9
+ import hashlib
10
+ import secrets
11
+ from dataclasses import dataclass
12
+ from datetime import timedelta
13
+
14
+ from django.conf import settings
15
+ from django.http import HttpRequest
16
+ from django.utils import timezone
17
+ from ninja.security import HttpBearer
18
+
19
+ from secondshift.workspaces.access import (
20
+ API_TOKEN_ATTRIBUTE,
21
+ MEMBERSHIP_ATTRIBUTE,
22
+ TOKEN_ACTIONS,
23
+ role_allows,
24
+ )
25
+ from secondshift.workspaces.dao import (
26
+ api_token_by_hash,
27
+ create_token_record,
28
+ touch_token,
29
+ )
30
+ from secondshift.workspaces.models import ApiToken, WorkspaceMembership
31
+
32
+ # Secret scanners can match tokens by this prefix.
33
+ TOKEN_PREFIX = "ss_"
34
+ # Last use is saved at most this often, so each call does not write.
35
+ LAST_USE_PRECISION = timedelta(minutes=1)
36
+
37
+
38
+ class TokenError(Exception):
39
+ pass
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class NewApiToken:
44
+ token: ApiToken
45
+ # Only here, right after creating it. It is never stored.
46
+ secret: str
47
+
48
+
49
+ def token_hash(secret: str) -> str:
50
+ return hashlib.sha256(secret.encode()).hexdigest()
51
+
52
+
53
+ def scope_allowed(role: str, scope: str) -> bool:
54
+ """Whether a role may create a token with this scope."""
55
+ return all(
56
+ role_allows(role, action) for action in TOKEN_ACTIONS[ApiToken.Scope(scope)]
57
+ )
58
+
59
+
60
+ def create_api_token(
61
+ membership: WorkspaceMembership, name: str, scope: str, days: int
62
+ ) -> NewApiToken:
63
+ if not 1 <= days <= settings.API_TOKEN_MAX_DAYS:
64
+ raise TokenError(
65
+ f"Choose an expiry between 1 and {settings.API_TOKEN_MAX_DAYS} days."
66
+ )
67
+ if not scope_allowed(membership.role, scope):
68
+ raise TokenError("Only workspace owners and admins can create run tokens.")
69
+ secret = TOKEN_PREFIX + secrets.token_urlsafe(32)
70
+ token = create_token_record(
71
+ membership,
72
+ name,
73
+ ApiToken.Scope(scope),
74
+ token_hash(secret),
75
+ timezone.now() + timedelta(days=days),
76
+ )
77
+ return NewApiToken(token, secret)
78
+
79
+
80
+ def revoke_api_token(token: ApiToken) -> None:
81
+ token.revoked_at = timezone.now()
82
+ token.save(update_fields=["revoked_at"])
83
+
84
+
85
+ def find_api_token(secret: str) -> ApiToken | None:
86
+ """The token for a secret while it works: usable, creator still a member."""
87
+ return api_token_by_hash(token_hash(secret))
88
+
89
+
90
+ def record_use(token: ApiToken) -> None:
91
+ now = timezone.now()
92
+ touch_token(token, now, now - LAST_USE_PRECISION)
93
+
94
+
95
+ class ApiTokenAuth(HttpBearer):
96
+ """Sign the request in as the token's creator, in the token's workspace."""
97
+
98
+ def authenticate(self, request: HttpRequest, token: str) -> ApiToken | None:
99
+ api_token = find_api_token(token)
100
+ if api_token is None:
101
+ return None
102
+ request.user = api_token.created_by
103
+ setattr(request, API_TOKEN_ATTRIBUTE, api_token)
104
+ # Forget any membership resolved from the session before this point.
105
+ if hasattr(request, MEMBERSHIP_ATTRIBUTE):
106
+ delattr(request, MEMBERSHIP_ATTRIBUTE)
107
+ record_use(api_token)
108
+ return api_token
@@ -0,0 +1,32 @@
1
+ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
2
+ from django.shortcuts import render
3
+ from django.urls import reverse
4
+ from django.views.decorators.cache import never_cache
5
+ from django.views.decorators.http import require_GET
6
+
7
+ from secondshift.workspaces.invites import (
8
+ INVITE_UNUSABLE_MESSAGE,
9
+ find_invite,
10
+ remember_invite,
11
+ )
12
+
13
+
14
+ @never_cache
15
+ @require_GET
16
+ def open_invite(request: HttpRequest, token: str) -> HttpResponse:
17
+ """Remember the invite, then send the person to sign up or to the app.
18
+
19
+ Opening a link never joins a workspace. The dashboard asks the signed-in
20
+ person to confirm first.
21
+ """
22
+ if find_invite(token) is None:
23
+ return render(
24
+ request,
25
+ "workspaces/invite_unusable.html",
26
+ {"message": INVITE_UNUSABLE_MESSAGE},
27
+ status=400,
28
+ )
29
+ remember_invite(request, token)
30
+ if request.user.is_authenticated:
31
+ return HttpResponseRedirect("/")
32
+ return HttpResponseRedirect(reverse("account_signup"))
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.5
2
+ Name: secondshift
3
+ Version: 0.1.0b12
4
+ Summary: Unattended agent jobs for your team, defined in Git.
5
+ Project-URL: Homepage, https://second-shift.app/
6
+ Project-URL: Documentation, https://second-shift.app/docs/overview/
7
+ Project-URL: Repository, https://github.com/piotrekno1/second-shift
8
+ Project-URL: Issues, https://github.com/piotrekno1/second-shift/issues
9
+ Project-URL: Changelog, https://github.com/piotrekno1/second-shift/releases
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Requires-Python: <3.14,>=3.13
13
+ Requires-Dist: anthropic>=1.15.0
14
+ Requires-Dist: anyio>=4.15.1
15
+ Requires-Dist: certifi>=2026.10.19
16
+ Requires-Dist: croniter>=6.0.0
17
+ Requires-Dist: cryptography>=46.0.0
18
+ Requires-Dist: dbos>=3.3.0
19
+ Requires-Dist: django-allauth>=65.0.0
20
+ Requires-Dist: django-axes>=8.3.1
21
+ Requires-Dist: django-click>=2.4.0
22
+ Requires-Dist: django-cors-headers>=4.9.0
23
+ Requires-Dist: django-encrypted-model-fields>=0.6.5
24
+ Requires-Dist: django-environ>=0.13.0
25
+ Requires-Dist: django-health-check>=4.8.0
26
+ Requires-Dist: django-ninja>=1.5.0
27
+ Requires-Dist: django-storages[s3]>=1.14.6
28
+ Requires-Dist: django<6.1,>=6.0
29
+ Requires-Dist: genai-prices>=0.1.13
30
+ Requires-Dist: gunicorn>=23.0.0
31
+ Requires-Dist: httpcore>=1.0.9
32
+ Requires-Dist: httpx>=0.28.1
33
+ Requires-Dist: jsonschema<5,>=4.26
34
+ Requires-Dist: mcp<2,>=1.26
35
+ Requires-Dist: psycopg[binary,pool]>=3.2.0
36
+ Requires-Dist: pydantic-ai-slim[anthropic,dbos,openai]>=2.57.0
37
+ Requires-Dist: pyyaml>=6.0.2
38
+ Requires-Dist: redis>=6
39
+ Requires-Dist: thin-mcp[django]<0.3,>=0.2
40
+ Requires-Dist: uvicorn-worker>=0.4.0
41
+ Requires-Dist: whitenoise>=6.11.0
42
+ Description-Content-Type: text/markdown
43
+
44
+ # SecondShift
45
+
46
+ **Unattended agent jobs for your team, without giving the agent your service
47
+ credentials.**
48
+
49
+ Define a job once in your repository. SecondShift runs it on a schedule, when
50
+ an alert or other webhook arrives, or when anyone on the team asks from the
51
+ dashboard or their coding agent. The agent works in a sandbox with no network
52
+ and no service credentials. Every tool call goes through a broker that checks
53
+ it against what a workspace owner approved. Results land where your team
54
+ already works, such as Slack, a GitHub issue or a draft PR.
55
+
56
+ Built for platform, security and SRE teams who want agents doing recurring
57
+ work without handing them the keys. Runs on your laptop with one command, or
58
+ on your own server in production.
59
+
60
+ [Quick start](#quick-start) · [Skill catalog](examples/README.md) ·
61
+ [Security model](docs/security-model.md) ·
62
+ [Docs](https://second-shift.app/docs/overview/)
63
+
64
+ https://github.com/user-attachments/assets/69cbc92f-72f6-45fb-98ad-27b4b1056581
65
+
66
+ ## Quick start
67
+
68
+ You need [uv](https://docs.astral.sh/uv/getting-started/installation/), Docker,
69
+ and a model API key or a Claude Code or Codex subscription. Start Docker, then
70
+ run:
71
+
72
+ ```bash
73
+ release=https://github.com/piotrekno1/second-shift/releases
74
+ uv tool install --force --python 3.13 \
75
+ "$release/download/v0.1.0b12/secondshift-0.1.0b12-py3-none-any.whl"
76
+ secondshift up
77
+ ```
78
+
79
+ The first start builds the agent image for subscriptions and takes a few
80
+ minutes. Use `secondshift up --no-agents` to skip it. Your dashboard opens when
81
+ SecondShift is ready.
82
+
83
+ Try it on this repository. SecondShift reviews its own documentation:
84
+
85
+ 1. Clone it: `git clone https://github.com/piotrekno1/second-shift.git`
86
+ 2. In **Repositories**, choose **Add repository**, then **Local folder**, and
87
+ select the clone.
88
+ 3. In **Models**, add an API key or a Claude Code or Codex subscription.
89
+ 4. In **Jobs**, open **Documentation drift** and click **Run now**.
90
+
91
+ For one-off work, open a repository, click **New task** and write what to do.
92
+ To connect your coding agent, open **Connect your agent** in the account menu.
93
+
94
+ ### Try a skill from GitHub in the sandbox
95
+
96
+ A skill from the internet is someone else's instructions, and many come with
97
+ scripts. Run one in your own coding agent and those scripts can reach whatever
98
+ your machine can. In SecondShift its commands run in the sandbox, with no
99
+ network and no credentials.
100
+
101
+ Open a repository, click **New task**, then **Skill from GitHub**, and paste a
102
+ link to a `SKILL.md`, such as one from
103
+ [anthropics/skills](https://github.com/anthropics/skills). SecondShift pins it
104
+ to a commit, shows what it asks for, and mounts it read-only. If you like the
105
+ result, click **Run again** or **Schedule this skill**. See
106
+ [Try a skill from GitHub](docs/tasks.md#try-a-skill-from-github).
107
+
108
+ ## The problem
109
+
110
+ Your team asks agents for the same work every week. Triage the CI failures.
111
+ Check which docs went stale. Look into the alert that fired at 3am. Each time,
112
+ someone writes the prompt again, waits for the answer and pays for the tokens.
113
+ Often two people pay for the same answer.
114
+
115
+ Automating it means letting agents run unattended. The usual way is a cron
116
+ workflow in CI. The agent runs on the runner that holds your keys, with open
117
+ network access. One prompt injection in a log line or an issue reaches
118
+ everything that runner can. Token spend has no ceiling, and each repository
119
+ wires up its own workflow.
120
+
121
+ So teams keep prompting by hand, or accept these risks.
122
+
123
+ With SecondShift you define the work once. It runs on a schedule or when an
124
+ event arrives, once for the whole team. The agent never holds your service
125
+ credentials, and it can call only the tools an owner approved. The latest result is
126
+ already waiting in Slack, or in your coding agent over MCP.
127
+
128
+ ## Who it's for
129
+
130
+ Platform, security and SRE teams at companies that already use agents and now
131
+ want them to run unattended. SecondShift fits when:
132
+
133
+ - your security review won't let an agent hold production credentials,
134
+ - several teams want recurring jobs with one set of approvals and one record,
135
+ - you need to choose the model or keep data on your own servers.
136
+
137
+ For a few personal jobs, Claude Code scheduled tasks or a GitHub Actions
138
+ workflow may be enough. See [Alternatives](docs/comparisons.md).
139
+
140
+ ## How it works
141
+
142
+ A job is a skill plus a few lines of YAML in `.secondshift.yaml`:
143
+
144
+ ```yaml
145
+ jobs:
146
+ alert-triage:
147
+ name: Alert triage
148
+ skill: .agents/skills/alert-triage/SKILL.md
149
+ triggers: [webhook]
150
+ webhook:
151
+ skip_when: { status: resolved }
152
+ repeat_key: groupKey
153
+ repeat_window: 1h
154
+ requires:
155
+ mcps:
156
+ - binding: grafana
157
+ tools: [query_prometheus, query_loki_logs, alerting_rules_read]
158
+ - binding: sentry
159
+ tools: [search_issues, search_errors]
160
+ notify:
161
+ - binding: alerts-slack
162
+ ```
163
+
164
+ 1. **Merge it.** SecondShift syncs the repository and finds the job.
165
+ 2. **Approve access.** The job asks for tools by name. A workspace owner
166
+ decides which connection provides them and can restrict their arguments.
167
+ The YAML never holds a credential, endpoint or token.
168
+ 3. **It runs.** On a schedule, from a webhook, or on demand. The agent works in
169
+ a networkless sandbox on a snapshot of the commit. Tool calls go through a
170
+ broker that checks each one against the approved plan.
171
+ 4. **It delivers.** SecondShift posts the report, opens the issues or the draft
172
+ pull request, and keeps the run's full record.
173
+
174
+ ## What you get
175
+
176
+ **Work defined in Git.** Skills and schedules are files in the repository.
177
+ Changes go through pull requests and code owners. Every run records the commit
178
+ it ran and the exact access it had.
179
+
180
+ **Access an owner approves.** Repositories request capabilities. Owners grant
181
+ them per job, per tool, and can fix, limit or forbid individual arguments. An
182
+ owner can also make the merge itself the approval for tools a connection
183
+ already allows.
184
+
185
+ **No secrets in the sandbox.** The agent runs with no network and no
186
+ credentials. SecondShift makes tool calls, opens pull requests and files
187
+ issues on its behalf. The model is always isolated from keys.
188
+
189
+ **Three ways to start work.**
190
+ - *Schedules* for recurring reviews and briefings.
191
+ - *Webhooks* for events such as alerts, with signed requests and filters for
192
+ resolved and repeated alerts.
193
+ - *One-off tasks* from the dashboard or your coding agent, queued per
194
+ repository and optionally delayed. A task can run a skill from any public
195
+ GitHub repository, pinned to a commit.
196
+
197
+ **Output your team can act on.** Reports to Slack or any webhook, with retry.
198
+ Files the agent saved, such as CSVs and images. GitHub issues, deduplicated and
199
+ capped per run. Draft pull requests on a `secondshift/run-<id>` branch. Jobs
200
+ can read their own earlier reports, so a weekly review knows what it said last
201
+ week.
202
+
203
+ **Your choice of model.** Use an API key from a model provider, or a local
204
+ model. You can also run jobs through the official Claude Code or Codex CLI,
205
+ signed in with your own account. Check that your plan's terms cover it. For
206
+ team jobs in production, use an API key.
207
+
208
+ **Costs with a ceiling.** Each run has a token budget and a tool-call limit.
209
+ The gateway cuts a run off when its budget is used.
210
+
211
+ **History you can see.** Each run shows its trigger, commit, tool calls, token
212
+ usage and model cost. Runs are durable workflows. They survive
213
+ restarts, and retries don't repeat tool calls.
214
+
215
+ **Works with your coding agent.** SecondShift is an MCP server. Claude Code,
216
+ Codex, Cursor and other clients can check a `.secondshift.yaml` before you
217
+ commit it, start approved jobs, submit tasks and read reports. Tokens are
218
+ scoped to a person and a workspace and can never approve access.
219
+
220
+ ## What teams run
221
+
222
+ The [skill catalog](examples/README.md) has example skills to copy into your
223
+ repository and adapt.
224
+
225
+ | Team | Examples |
226
+ | --- | --- |
227
+ | Developers | [Repository health](examples/repository-health/README.md), [docs drift](examples/docs-drift/README.md), [implicit state machines](examples/implicit-state-machines/README.md), [CI failures](examples/ci-failure-review/README.md) |
228
+ | SRE and operations | [Alert triage](examples/alert-triage/README.md), [production errors](examples/production-error-review/README.md), [monitoring coverage](examples/monitoring-coverage-review/README.md), [on-call coverage](examples/on-call-coverage/README.md) |
229
+ | Security and compliance | [Security regression](examples/security-regression/README.md), [quarterly access review](examples/quarterly-access-review/README.md) |
230
+ | Engineering management | [Monday briefing](examples/engineering-briefing/README.md), [release readiness](examples/release-readiness-review/README.md), [ticket reconciliation](examples/ticket-reconciliation/README.md) |
231
+
232
+ ## Run it in production
233
+
234
+ The [deployment guide](docs/production-deployment.md) covers a single-server
235
+ Docker Compose setup with HTTPS, PostgreSQL, S3 or local artifact storage, and
236
+ gVisor. It includes tested backup, restore and upgrade steps.
237
+
238
+ See [GitHub repository access](docs/github-app.md) to set up access with a
239
+ GitHub App or a personal access token.
240
+
241
+ ## Security
242
+
243
+ | What | Where it lives during a run | Never reaches |
244
+ | --- | --- | --- |
245
+ | Model provider key | Gateway | Agent loop, sandbox, model |
246
+ | MCP and API credentials | Worker or gateway, per broker call | Sandbox, model |
247
+ | GitHub write token | Worker, after the sandbox stops | Sandbox, model |
248
+ | Subscription login | Runner, sealed to its key | Container arguments, environment, logs |
249
+
250
+ SecondShift treats repository content, model output and tool results as
251
+ untrusted.
252
+
253
+ Approving a write tool means trusting the agent with it. The
254
+ [security model](docs/security-model.md) covers this and the other risks that
255
+ remain.
256
+
257
+ ## Status
258
+
259
+ SecondShift is in beta. See the [release notes](docs/releases/) for what
260
+ changed and what was tested.
261
+
262
+ ## Learn more
263
+
264
+ - [Repository configuration](docs/repository-configuration.md)
265
+ - [Tasks](docs/tasks.md)
266
+ - [Agent subscriptions](docs/agent-subscriptions.md)
267
+ - [MCP server](docs/mcp.md) and [vendor MCP servers](docs/mcp-recipes.md)
268
+ - [Architecture](docs/architecture.md)
269
+ - [Alternatives](docs/comparisons.md)
270
+ - [Contributing](CONTRIBUTING.md)