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,83 @@
1
+ from django.contrib import admin
2
+ from django.http import HttpRequest
3
+ from django.utils import timezone
4
+
5
+ from secondshift.jobs.models import CapabilityRequest, Job, JobGrant
6
+
7
+
8
+ class CapabilityRequestInline(admin.TabularInline):
9
+ model = CapabilityRequest
10
+ extra = 0
11
+ can_delete = False
12
+ readonly_fields = (
13
+ "binding",
14
+ "kind",
15
+ "requested_tools",
16
+ "required",
17
+ "created_at",
18
+ "updated_at",
19
+ )
20
+
21
+
22
+ @admin.register(Job)
23
+ class JobAdmin(admin.ModelAdmin):
24
+ list_display = (
25
+ "name",
26
+ "repository",
27
+ "cron",
28
+ "timezone",
29
+ "enabled",
30
+ "next_run_at",
31
+ )
32
+ list_filter = ("enabled", "timezone")
33
+ search_fields = ("name", "key", "repository__full_name", "skill_path")
34
+ readonly_fields = ("config_hash", "created_at", "updated_at")
35
+ inlines = [CapabilityRequestInline]
36
+
37
+
38
+ @admin.register(CapabilityRequest)
39
+ class CapabilityRequestAdmin(admin.ModelAdmin):
40
+ list_display = ("binding", "kind", "job", "required", "grant_status")
41
+ list_filter = ("kind", "required")
42
+ search_fields = ("binding", "job__name", "job__repository__full_name")
43
+ readonly_fields = (
44
+ "job",
45
+ "binding",
46
+ "kind",
47
+ "requested_tools",
48
+ "required",
49
+ "created_at",
50
+ "updated_at",
51
+ )
52
+
53
+ @admin.display(description="Grant")
54
+ def grant_status(self, request: CapabilityRequest) -> str:
55
+ grant = getattr(request, "grant", None)
56
+ return grant.status if grant else "unbound"
57
+
58
+
59
+ @admin.register(JobGrant)
60
+ class JobGrantAdmin(admin.ModelAdmin):
61
+ list_display = ("capability_request", "connection", "status", "reviewed_at")
62
+ list_filter = ("status", "connection__kind")
63
+ search_fields = (
64
+ "capability_request__binding",
65
+ "capability_request__job__name",
66
+ "connection__display_name",
67
+ )
68
+ readonly_fields = ("reviewed_by", "reviewed_at", "created_at", "updated_at")
69
+
70
+ def save_model(
71
+ self,
72
+ request: HttpRequest,
73
+ obj: JobGrant,
74
+ form: object,
75
+ change: bool,
76
+ ) -> None:
77
+ if obj.status in {JobGrant.Status.APPROVED, JobGrant.Status.DENIED}:
78
+ obj.reviewed_by = request.user
79
+ obj.reviewed_at = timezone.now()
80
+ else:
81
+ obj.reviewed_by = None
82
+ obj.reviewed_at = None
83
+ super().save_model(request, obj, form, change)
@@ -0,0 +1,184 @@
1
+ from django.http import HttpRequest
2
+ from django.shortcuts import get_object_or_404
3
+ from ninja import Router
4
+ from ninja.errors import HttpError
5
+ from ninja.security import django_auth
6
+
7
+ from secondshift.connections.dao import workspace_connections
8
+ from secondshift.connections.serialization import serialize_connection
9
+ from secondshift.jobs.access import job_for_request
10
+ from secondshift.jobs.dao import job_requests
11
+ from secondshift.jobs.grants import GrantError, decide_grant
12
+ from secondshift.jobs.models import JOB_REMOVED
13
+ from secondshift.jobs.needs import capability_needs
14
+ from secondshift.jobs.planning import build_execution_plan
15
+ from secondshift.jobs.readiness import execution_problems
16
+ from secondshift.jobs.scheduling import restart_schedules
17
+ from secondshift.jobs.schemas import (
18
+ CapabilityNeedSchema,
19
+ GrantDecisionSchema,
20
+ JobDetailSchema,
21
+ JobEnabledSchema,
22
+ JobSchema,
23
+ NeedJobSchema,
24
+ SkillInspectionInput,
25
+ SkillInspectionSchema,
26
+ )
27
+ from secondshift.jobs.serialization import (
28
+ serialize_inspection,
29
+ serialize_job,
30
+ serialize_job_detail,
31
+ serialize_workspace_jobs,
32
+ )
33
+ from secondshift.jobs.skill_needs import inspect_external_skill
34
+ from secondshift.repositories.access import repository_for_request
35
+ from secondshift.repositories.external_skills import ExternalSkillError
36
+ from secondshift.workspaces.access import (
37
+ Action,
38
+ allows,
39
+ current_workspace,
40
+ require,
41
+ )
42
+ from secondshift.workspaces.limits import LimitError, save_overrides
43
+ from secondshift.workspaces.schemas import RunLimitsUpdateSchema
44
+ from secondshift.workspaces.serialization import limit_error_detail
45
+
46
+ router = Router(auth=django_auth, tags=["jobs"])
47
+
48
+
49
+ @router.get("/capability-needs", response=list[CapabilityNeedSchema], auth=django_auth)
50
+ def needs(request: HttpRequest) -> list[CapabilityNeedSchema]:
51
+ workspace = current_workspace(request)
52
+ if not workspace:
53
+ return []
54
+ return [
55
+ CapabilityNeedSchema(
56
+ binding=need.binding,
57
+ kind=need.kind,
58
+ required=need.required,
59
+ requested_tools=need.requested_tools,
60
+ status=need.status,
61
+ detail=need.detail,
62
+ connection=(
63
+ serialize_connection(need.connection) if need.connection else None
64
+ ),
65
+ jobs=[
66
+ NeedJobSchema(
67
+ **{
68
+ **vars(job),
69
+ "access": vars(job.access) if job.access else None,
70
+ }
71
+ )
72
+ for job in need.jobs
73
+ ],
74
+ )
75
+ for need in capability_needs(workspace)
76
+ ]
77
+
78
+
79
+ @router.get("/jobs", response=list[JobSchema])
80
+ def jobs(request: HttpRequest) -> list[JobSchema]:
81
+ workspace = current_workspace(request)
82
+ return serialize_workspace_jobs(workspace) if workspace else []
83
+
84
+
85
+ @router.get("/jobs/{job_id}", response=JobDetailSchema)
86
+ def job_detail(request: HttpRequest, job_id: int) -> JobDetailSchema:
87
+ job = job_for_request(request, job_id)
88
+ return serialize_job_detail(job, allows(request, Action.CHANGE_RUN_LIMITS))
89
+
90
+
91
+ @router.post("/jobs/{job_id}/limits", response=JobDetailSchema)
92
+ def set_job_limits(
93
+ request: HttpRequest, job_id: int, payload: RunLimitsUpdateSchema
94
+ ) -> JobDetailSchema:
95
+ """Change the run limits of one job. Runs already created keep theirs."""
96
+ job = job_for_request(request, job_id)
97
+ require(request, Action.CHANGE_RUN_LIMITS)
98
+ try:
99
+ save_overrides(job, payload.model_dump(exclude_unset=True))
100
+ except LimitError as error:
101
+ raise HttpError(422, limit_error_detail(request, error)) from error
102
+ return serialize_job_detail(job_for_request(request, job_id), True)
103
+
104
+
105
+ @router.post("/jobs/{job_id}/enabled", response=JobSchema)
106
+ def set_job_enabled(
107
+ request: HttpRequest, job_id: int, payload: JobEnabledSchema
108
+ ) -> JobSchema:
109
+ job = job_for_request(request, job_id)
110
+ require(request, Action.ENABLE_JOBS)
111
+ if payload.enabled:
112
+ if job.removed_at is not None:
113
+ raise HttpError(409, JOB_REMOVED)
114
+ if not job.cron:
115
+ raise HttpError(409, "This job has no schedule. Use Run now to run it.")
116
+ if not job.repository.accepts_runs:
117
+ raise HttpError(409, "Enable the repository before enabling this job.")
118
+ plan = build_execution_plan(job)
119
+ if not plan.ready:
120
+ raise HttpError(409, " ".join(execution_problems(job.repository, plan)))
121
+ turned_on = payload.enabled and not job.enabled
122
+ job.enabled = payload.enabled
123
+ job.save(update_fields=["enabled", "updated_at"])
124
+ if turned_on:
125
+ restart_schedules([job])
126
+ return serialize_job(job_for_request(request, job_id))
127
+
128
+
129
+ @router.post("/jobs/{job_id}/capabilities/{capability_id}/grant", response=JobSchema)
130
+ def review_job_capability(
131
+ request: HttpRequest,
132
+ job_id: int,
133
+ capability_id: int,
134
+ payload: GrantDecisionSchema,
135
+ ) -> JobSchema:
136
+ job = job_for_request(request, job_id)
137
+ require(request, Action.REVIEW_CAPABILITIES)
138
+ capability = get_object_or_404(job_requests(job), pk=capability_id)
139
+ connection = (
140
+ None
141
+ if payload.connection_id is None
142
+ else get_object_or_404(
143
+ workspace_connections(job.workspace_id), pk=payload.connection_id
144
+ )
145
+ )
146
+ try:
147
+ decide_grant(
148
+ capability,
149
+ payload.decision,
150
+ connection,
151
+ payload.allowed_tools,
152
+ request.user,
153
+ (
154
+ None
155
+ if payload.tool_rules is None
156
+ else {
157
+ tool: {name: rule.dict() for name, rule in rules.items()}
158
+ for tool, rules in payload.tool_rules.items()
159
+ }
160
+ ),
161
+ )
162
+ except GrantError as error:
163
+ raise HttpError(error.status, str(error)) from error
164
+ return serialize_job(job_for_request(request, job_id))
165
+
166
+
167
+ @router.post("/skills/inspect", response=SkillInspectionSchema)
168
+ def inspect_skill(
169
+ request: HttpRequest, payload: SkillInspectionInput
170
+ ) -> SkillInspectionSchema:
171
+ """Fetch a skill from another public repository and show what it needs."""
172
+ workspace = require(request, Action.RUN_JOBS)
173
+ target = (
174
+ repository_for_request(request, payload.repository_id)
175
+ if payload.repository_id is not None
176
+ else None
177
+ )
178
+ try:
179
+ inspection = inspect_external_skill(
180
+ workspace, payload.repository, payload.path, payload.ref, target
181
+ )
182
+ except ExternalSkillError as error:
183
+ raise HttpError(422, str(error)) from error
184
+ return serialize_inspection(inspection)
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class JobsConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "secondshift.jobs"
@@ -0,0 +1,115 @@
1
+ """Grants approved without an owner, for connections that do not require approval.
2
+
3
+ When a connection's owner turns approval off, merging `.secondshift.yaml` is
4
+ the review. A job still gets only tools the owner allowed on the connection.
5
+ Anything else waits for an owner, as before. Owner decisions are kept: an
6
+ approved or denied grant is never changed here.
7
+ """
8
+
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+
12
+ from secondshift.connections.dao import auto_approved_connections
13
+ from secondshift.connections.external import external_access
14
+ from secondshift.connections.models import Connection
15
+ from secondshift.jobs.capabilities import RequestedCapability
16
+ from secondshift.jobs.dao import (
17
+ binding_active_requests,
18
+ create_grants,
19
+ save_grant_reviews,
20
+ )
21
+ from secondshift.jobs.models import CapabilityRequest, JobGrant
22
+ from secondshift.repositories.github import repository_ref
23
+ from secondshift.repositories.models import Repository
24
+
25
+
26
+ def approves_automatically(
27
+ request: CapabilityRequest | RequestedCapability,
28
+ connection: Connection,
29
+ repository: Repository,
30
+ ) -> bool:
31
+ if connection.workspace_id != repository.workspace_id:
32
+ return False
33
+ if connection.require_approval:
34
+ return False
35
+ if not connection.is_connected:
36
+ return False
37
+ if connection.kind != request.kind or connection.binding_key != request.binding:
38
+ return False
39
+ if set(request.requested_tools) - set(connection.available_tools):
40
+ return False
41
+ access = external_access(connection, repository_ref(repository))
42
+ return access is None or access.ok
43
+
44
+
45
+ def automatic_approval(
46
+ request: CapabilityRequest, connection: Connection, repository: Repository
47
+ ) -> dict[str, object] | None:
48
+ """The grant fields that approve the request, or None if it must wait."""
49
+ if not approves_automatically(request, connection, repository):
50
+ return None
51
+ return {
52
+ "connection_id": connection.pk,
53
+ "status": JobGrant.Status.APPROVED,
54
+ "allowed_tools": sorted(request.requested_tools),
55
+ "reviewed_by_id": None,
56
+ "reviewed_at": timezone.now(),
57
+ }
58
+
59
+
60
+ def approve_waiting(loaded: list[CapabilityRequest]) -> int:
61
+ """Approve the waiting requests that their connections allow. Returns how many.
62
+
63
+ Each request needs its grant and its job's repository loaded.
64
+ """
65
+ if not loaded:
66
+ return 0
67
+ connections = {
68
+ (connection.workspace_id, connection.binding_key): connection
69
+ for connection in auto_approved_connections(
70
+ {request.job.workspace_id for request in loaded},
71
+ {request.binding for request in loaded},
72
+ )
73
+ }
74
+ now = timezone.now()
75
+ created: list[JobGrant] = []
76
+ updated: list[JobGrant] = []
77
+ for request in loaded:
78
+ grant = getattr(request, "grant", None)
79
+ if grant is not None and grant.status != JobGrant.Status.PENDING:
80
+ continue
81
+ connection = connections.get((request.job.workspace_id, request.binding))
82
+ if connection is None:
83
+ continue
84
+ fields = automatic_approval(request, connection, request.job.repository)
85
+ if fields is None:
86
+ continue
87
+ if grant is None:
88
+ grant = JobGrant(capability_request=request)
89
+ created.append(grant)
90
+ else:
91
+ updated.append(grant)
92
+ for name, value in {**fields, "updated_at": now}.items():
93
+ setattr(grant, name, value)
94
+ create_grants(created)
95
+ save_grant_reviews(updated)
96
+ return len(created) + len(updated)
97
+
98
+
99
+ def approve_waiting_for(connection: Connection) -> int:
100
+ """Approve requests that were waiting for this connection."""
101
+ if connection.require_approval or not connection.binding_key:
102
+ return 0
103
+ return approve_waiting(
104
+ binding_active_requests(connection.workspace_id, connection.binding_key)
105
+ )
106
+
107
+
108
+ @transaction.atomic
109
+ def set_require_approval(connection: Connection, require: bool) -> Connection:
110
+ """Change the setting. Existing grants stay."""
111
+ if connection.require_approval != require:
112
+ connection.require_approval = require
113
+ connection.save(update_fields=["require_approval", "updated_at"])
114
+ approve_waiting_for(connection)
115
+ return connection
@@ -0,0 +1,78 @@
1
+ """Sync repositories whose branch moved, so merged changes apply.
2
+
3
+ Syncing only applies what is committed on the branch. New access a job
4
+ requests still waits for an owner's approval. A repository added before it
5
+ had a valid configuration is checked even while disabled, so the first
6
+ committed .secondshift.yaml shows up without a manual Resync.
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ from django.conf import settings
13
+
14
+ from secondshift.connections.dao import has_github_token_sources
15
+ from secondshift.connections.github import can_check
16
+ from secondshift.github import repos
17
+ from secondshift.github.app import configured
18
+ from secondshift.github.errors import GitHubError
19
+ from secondshift.jobs.repository_sync import sync_repository
20
+ from secondshift.repositories.dao import repositories_to_sync, set_sync_error
21
+ from secondshift.repositories.github_access import github_access
22
+ from secondshift.repositories.github_check import check_after_failure
23
+ from secondshift.repositories.local import (
24
+ LocalRepositoryError,
25
+ branch_revision,
26
+ )
27
+ from secondshift.repositories.models import Repository
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def server_syncs() -> bool:
33
+ """Whether the worker syncs at all on this server."""
34
+ return bool(
35
+ (
36
+ settings.LOCAL_REPOSITORIES_ENABLED
37
+ or configured()
38
+ or has_github_token_sources()
39
+ )
40
+ and settings.REPOSITORY_SYNC_INTERVAL_SECONDS > 0
41
+ )
42
+
43
+
44
+ def branch_head(repository: Repository) -> str:
45
+ if repository.source_type == Repository.SourceType.GITHUB:
46
+ access = github_access(repository)
47
+ return repos.branch_head(
48
+ access.token, access.repository_id, repository.default_branch
49
+ )
50
+ return branch_revision(Path(repository.local_path), repository.default_branch)
51
+
52
+
53
+ def sync_moved_repositories() -> int:
54
+ """Sync each repository whose branch has a new commit. Returns how many."""
55
+ synced = 0
56
+ for repository in repositories_to_sync(settings.LOCAL_REPOSITORIES_ENABLED):
57
+ is_github = repository.source_type == Repository.SourceType.GITHUB
58
+ if is_github and not can_check(repository.github_connection):
59
+ continue
60
+ try:
61
+ if branch_head(repository) == repository.checked_sha:
62
+ continue
63
+ result = sync_repository(repository)
64
+ except (LocalRepositoryError, GitHubError) as error:
65
+ # The repository page shows the same problem when opened. Runs
66
+ # keep using the last good sync until it is fixed.
67
+ set_sync_error(repository.pk, str(error))
68
+ logger.warning("could not sync %s: %s", repository.display_name, error)
69
+ if is_github:
70
+ check_after_failure(repository.github_connection)
71
+ continue
72
+ if result.last_sync_error:
73
+ logger.warning(
74
+ "could not sync %s: %s", repository.display_name, result.last_sync_error
75
+ )
76
+ continue
77
+ synced += 1
78
+ return synced
@@ -0,0 +1,43 @@
1
+ """Logical capability requests, independent of a saved job or its grants."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from secondshift.jobs.manifest import JobManifest
6
+ from secondshift.jobs.models import CapabilityRequest
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class RequestedCapability:
11
+ binding: str
12
+ kind: str
13
+ requested_tools: tuple[str, ...]
14
+ required: bool = True
15
+
16
+
17
+ def manifest_capabilities(definition: JobManifest) -> list[RequestedCapability]:
18
+ return [
19
+ *(
20
+ RequestedCapability(
21
+ item.binding,
22
+ CapabilityRequest.Kind.MCP,
23
+ tuple(item.tools),
24
+ item.required,
25
+ )
26
+ for item in definition.requires.mcps
27
+ ),
28
+ *(
29
+ RequestedCapability(
30
+ item.binding,
31
+ CapabilityRequest.Kind.API,
32
+ tuple(item.tools),
33
+ item.required,
34
+ )
35
+ for item in definition.requires.apis
36
+ ),
37
+ *(
38
+ RequestedCapability(
39
+ item.binding, CapabilityRequest.Kind.NOTIFICATION, (), item.required
40
+ )
41
+ for item in definition.notify
42
+ ),
43
+ ]