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,199 @@
1
+ from django.core.exceptions import ValidationError
2
+ from django.db import models
3
+
4
+ from secondshift.config.constants import MANIFEST_FILE
5
+ from secondshift.connections.models import Connection
6
+ from secondshift.jobs.manifest import OpenIssuesManifest, WebhookManifest
7
+ from secondshift.repositories.models import Repository
8
+ from secondshift.workspaces.models import RunLimitOverrides, Workspace
9
+
10
+ # Why a job removed from the manifest cannot run or be resumed.
11
+ JOB_REMOVED = f"This job is no longer in {MANIFEST_FILE}. Add it back to run it."
12
+
13
+
14
+ class Job(RunLimitOverrides):
15
+ workspace = models.ForeignKey(
16
+ Workspace,
17
+ on_delete=models.CASCADE,
18
+ related_name="jobs",
19
+ )
20
+ repository = models.ForeignKey(
21
+ Repository,
22
+ on_delete=models.CASCADE,
23
+ related_name="jobs",
24
+ )
25
+ key = models.SlugField(max_length=120)
26
+ name = models.CharField(max_length=200)
27
+ skill_path = models.CharField(max_length=500)
28
+ # {"repository": "owner/name", "ref": <commit SHA>} when the skill is in
29
+ # another public GitHub repository. skill_path is then a path there.
30
+ skill_source = models.JSONField(null=True, blank=True)
31
+ # The skill file at the synced commit, for people to read before a run.
32
+ # Runs read the file from the commit, never from here.
33
+ skill_text = models.TextField(blank=True, default="")
34
+ skill_truncated = models.BooleanField(default=False)
35
+ # Empty for a job that runs only when started manually.
36
+ cron = models.CharField(max_length=100, blank=True, default="")
37
+ timezone = models.CharField(max_length=64, default="UTC")
38
+ # How many of its own earlier reports each run starts with.
39
+ history = models.PositiveSmallIntegerField(default=0)
40
+ # How many sub-agents one run may start.
41
+ subagents = models.PositiveSmallIntegerField(default=0)
42
+ # Trigger kinds the repository names, such as "webhook". See JobTrigger.
43
+ triggers = models.JSONField(default=list, blank=True)
44
+ # The repository's webhook settings: skip_when, repeat_key, repeat_window.
45
+ webhook = models.JSONField(default=dict, blank=True)
46
+ # The repository asks for a draft pull request with each run's changes.
47
+ open_pull_request = models.BooleanField(default=False)
48
+ # The repository asks to open the issues each run's agent proposes:
49
+ # {"labels": [...], "max": n}. None when it does not.
50
+ open_issues = models.JSONField(null=True, blank=True)
51
+ enabled = models.BooleanField(default=False)
52
+ next_run_at = models.DateTimeField(null=True, blank=True)
53
+ # Set when a sync no longer finds the job in the manifest. The job and its
54
+ # runs stay for history, but it cannot run until the manifest names it again.
55
+ removed_at = models.DateTimeField(null=True, blank=True)
56
+ config_hash = models.CharField(max_length=64)
57
+ created_at = models.DateTimeField(auto_now_add=True)
58
+ updated_at = models.DateTimeField(auto_now=True)
59
+
60
+ class Meta(RunLimitOverrides.Meta):
61
+ constraints = [
62
+ models.UniqueConstraint(
63
+ fields=["repository", "key"],
64
+ name="unique_repository_job_key",
65
+ )
66
+ ]
67
+ ordering = ["repository__full_name", "name"]
68
+
69
+ def __str__(self) -> str:
70
+ return f"{self.repository}: {self.name}"
71
+
72
+ @property
73
+ def webhook_settings(self) -> WebhookManifest | None:
74
+ return WebhookManifest.model_validate(self.webhook) if self.webhook else None
75
+
76
+ @property
77
+ def issue_settings(self) -> OpenIssuesManifest | None:
78
+ if not self.open_issues:
79
+ return None
80
+ return OpenIssuesManifest.model_validate(self.open_issues)
81
+
82
+ def capability_grant_status(self) -> str:
83
+ """Summarize the required grants already loaded for this job."""
84
+ requests = [
85
+ request for request in self.capability_requests.all() if request.required
86
+ ]
87
+ statuses = [
88
+ getattr(request, "grant", None).status
89
+ if hasattr(request, "grant")
90
+ else JobGrant.Status.PENDING
91
+ for request in requests
92
+ ]
93
+ if JobGrant.Status.DENIED in statuses:
94
+ return JobGrant.Status.DENIED
95
+ if statuses and all(status == JobGrant.Status.APPROVED for status in statuses):
96
+ return JobGrant.Status.APPROVED
97
+ return JobGrant.Status.PENDING if requests else "not_required"
98
+
99
+
100
+ class CapabilityRequest(models.Model):
101
+ class Kind(models.TextChoices):
102
+ MCP = "mcp", "MCP"
103
+ API = "api", "API"
104
+ NOTIFICATION = "notification", "Notification"
105
+
106
+ job = models.ForeignKey(
107
+ Job,
108
+ on_delete=models.CASCADE,
109
+ related_name="capability_requests",
110
+ )
111
+ binding = models.SlugField(max_length=120)
112
+ kind = models.CharField(max_length=24, choices=Kind.choices)
113
+ requested_tools = models.JSONField(default=list, blank=True)
114
+ required = models.BooleanField(default=True)
115
+ created_at = models.DateTimeField(auto_now_add=True)
116
+ updated_at = models.DateTimeField(auto_now=True)
117
+
118
+ class Meta:
119
+ constraints = [
120
+ models.UniqueConstraint(
121
+ fields=["job", "binding"],
122
+ name="unique_job_capability_binding",
123
+ )
124
+ ]
125
+ ordering = ["job", "binding"]
126
+
127
+ def __str__(self) -> str:
128
+ return f"{self.job}: {self.binding}"
129
+
130
+
131
+ class JobGrant(models.Model):
132
+ class Status(models.TextChoices):
133
+ PENDING = "pending", "Pending"
134
+ APPROVED = "approved", "Approved"
135
+ DENIED = "denied", "Denied"
136
+
137
+ capability_request = models.OneToOneField(
138
+ CapabilityRequest,
139
+ on_delete=models.CASCADE,
140
+ related_name="grant",
141
+ )
142
+ connection = models.ForeignKey(
143
+ Connection,
144
+ on_delete=models.PROTECT,
145
+ related_name="job_grants",
146
+ null=True,
147
+ blank=True,
148
+ )
149
+ status = models.CharField(
150
+ max_length=16,
151
+ choices=Status.choices,
152
+ default=Status.PENDING,
153
+ )
154
+ allowed_tools = models.JSONField(default=list, blank=True)
155
+ # The owner's argument rules per approved tool. See jobs.tool_rules.
156
+ tool_rules = models.JSONField(default=dict, blank=True)
157
+ reviewed_by = models.ForeignKey(
158
+ "auth.User",
159
+ on_delete=models.PROTECT,
160
+ related_name="reviewed_job_grants",
161
+ null=True,
162
+ blank=True,
163
+ )
164
+ reviewed_at = models.DateTimeField(null=True, blank=True)
165
+ created_at = models.DateTimeField(auto_now_add=True)
166
+ updated_at = models.DateTimeField(auto_now=True)
167
+
168
+ class Meta:
169
+ ordering = ["capability_request__job", "capability_request__binding"]
170
+
171
+ def __str__(self) -> str:
172
+ return f"{self.capability_request}: {self.get_status_display()}"
173
+
174
+ def clean(self) -> None:
175
+ request = self.capability_request
176
+ connection = self.connection
177
+ if self.status == self.Status.APPROVED and connection is None:
178
+ raise ValidationError("Approved grants require a connection.")
179
+ if connection is None:
180
+ return
181
+ if request.job.workspace_id != connection.workspace_id:
182
+ raise ValidationError("Grant and connection must belong to one workspace.")
183
+ if request.kind != connection.kind:
184
+ raise ValidationError(
185
+ "Connection kind must match the requested capability."
186
+ )
187
+ if request.binding != connection.binding_key:
188
+ raise ValidationError(
189
+ "Connection binding must match the requested capability."
190
+ )
191
+ unknown_tools = set(self.allowed_tools) - set(request.requested_tools)
192
+ if unknown_tools:
193
+ raise ValidationError(
194
+ {"allowed_tools": "Allowed tools must be requested by the job."}
195
+ )
196
+ if set(self.tool_rules) - set(self.allowed_tools):
197
+ raise ValidationError(
198
+ {"tool_rules": "Rules can only apply to approved tools."}
199
+ )
@@ -0,0 +1,154 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Literal
3
+
4
+ from secondshift.connections.access import Access
5
+ from secondshift.connections.dao import connections_by_binding
6
+ from secondshift.connections.external import EXTERNAL_KINDS, external_access
7
+ from secondshift.connections.models import Connection
8
+ from secondshift.jobs.dao import workspace_requests
9
+ from secondshift.jobs.models import CapabilityRequest, JobGrant
10
+ from secondshift.repositories.github import repository_ref
11
+ from secondshift.repositories.models import Repository
12
+ from secondshift.workspaces.models import Workspace
13
+
14
+ NeedStatus = Literal["missing", "unavailable", "needs_approval", "ready"]
15
+ STATUS_ORDER: dict[str, int] = {
16
+ "missing": 0,
17
+ "unavailable": 1,
18
+ "needs_approval": 2,
19
+ "ready": 3,
20
+ }
21
+
22
+
23
+ @dataclass
24
+ class NeedJob:
25
+ job_id: int
26
+ job_name: str
27
+ repository: str
28
+ capability_id: int
29
+ required: bool
30
+ grant_status: str
31
+ access: Access | None = None
32
+
33
+
34
+ @dataclass
35
+ class CapabilityNeed:
36
+ binding: str
37
+ kind: str
38
+ required: bool
39
+ requested_tools: list[str]
40
+ status: NeedStatus
41
+ detail: str
42
+ connection: Connection | None
43
+ jobs: list[NeedJob] = field(default_factory=list)
44
+
45
+
46
+ def connection_problem(
47
+ kind: str,
48
+ connection: Connection | None,
49
+ requested_tools: set[str],
50
+ repositories: list[Repository],
51
+ ) -> tuple[NeedStatus, str] | None:
52
+ """Why a binding cannot be used yet, before anyone approves it."""
53
+ if connection is None:
54
+ return "missing", "No workspace connection uses this binding yet."
55
+ if connection.kind != kind:
56
+ return (
57
+ "unavailable",
58
+ f"The {connection.binding_key} connection is a "
59
+ f"{connection.get_kind_display()} connection, not {kind}.",
60
+ )
61
+ if (status := connection.current_status) != Connection.Status.CONNECTED:
62
+ return (
63
+ "unavailable",
64
+ f"The connection is {Connection.Status(status).label.lower()}.",
65
+ )
66
+ if kind in EXTERNAL_KINDS:
67
+ for repository in repositories:
68
+ access = external_access(connection, repository_ref(repository))
69
+ if access and not access.ok:
70
+ return "unavailable", access.text
71
+ missing_tools = requested_tools - set(connection.available_tools)
72
+ if missing_tools:
73
+ return (
74
+ "unavailable",
75
+ "The connection does not provide: " + ", ".join(sorted(missing_tools)),
76
+ )
77
+ return None
78
+
79
+
80
+ def need_status(
81
+ kind: str,
82
+ connection: Connection | None,
83
+ requests: list[CapabilityRequest],
84
+ requested_tools: set[str],
85
+ ) -> tuple[NeedStatus, str]:
86
+ problem = connection_problem(
87
+ kind,
88
+ connection,
89
+ requested_tools,
90
+ [request.job.repository for request in requests],
91
+ )
92
+ if problem:
93
+ return problem
94
+ unapproved = [
95
+ request
96
+ for request in requests
97
+ if getattr(request, "grant", None) is None
98
+ or request.grant.status == JobGrant.Status.PENDING
99
+ ]
100
+ if unapproved:
101
+ return "needs_approval", "Connected. Approve it for the jobs that use it."
102
+ return "ready", "Connected and reviewed for every job."
103
+
104
+
105
+ def capability_needs(workspace: Workspace) -> list[CapabilityNeed]:
106
+ """Group every capability requested by active jobs by its logical binding."""
107
+ requests = workspace_requests(workspace)
108
+ connections = connections_by_binding(workspace.pk)
109
+ grouped: dict[tuple[str, str], list[CapabilityRequest]] = {}
110
+ for request in requests:
111
+ grouped.setdefault((request.binding, request.kind), []).append(request)
112
+
113
+ needs: list[CapabilityNeed] = []
114
+ for (binding, kind), items in grouped.items():
115
+ connection = connections.get(binding)
116
+ requested_tools = {tool for item in items for tool in item.requested_tools}
117
+ status, detail = need_status(kind, connection, items, requested_tools)
118
+ needs.append(
119
+ CapabilityNeed(
120
+ binding=binding,
121
+ kind=kind,
122
+ required=any(item.required for item in items),
123
+ requested_tools=sorted(requested_tools),
124
+ status=status,
125
+ detail=detail,
126
+ connection=connection,
127
+ jobs=[
128
+ NeedJob(
129
+ job_id=item.job_id,
130
+ job_name=item.job.name,
131
+ repository=item.job.repository.display_name,
132
+ access=(
133
+ external_access(
134
+ connection, repository_ref(item.job.repository)
135
+ )
136
+ if connection and connection.kind == kind
137
+ else None
138
+ ),
139
+ capability_id=item.pk,
140
+ required=item.required,
141
+ grant_status=(
142
+ item.grant.status
143
+ if getattr(item, "grant", None)
144
+ else "unconfigured"
145
+ ),
146
+ )
147
+ for item in items
148
+ ],
149
+ )
150
+ )
151
+ needs.sort(
152
+ key=lambda need: (STATUS_ORDER[need.status], not need.required, need.binding)
153
+ )
154
+ return needs
@@ -0,0 +1,176 @@
1
+ """The execution plan frozen into each run.
2
+
3
+ The planner builds it once from server-owned policy. Runs, brokers and pages
4
+ read it through these types. It never holds credentials.
5
+ """
6
+
7
+ from enum import StrEnum
8
+ from pathlib import PurePosixPath
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, ConfigDict
12
+
13
+ from secondshift.config.constants import SANDBOX_SKILL_DIR, AgentBackend
14
+ from secondshift.workspaces.limits import RunLimit
15
+
16
+
17
+ class Frozen(BaseModel):
18
+ model_config = ConfigDict(frozen=True, extra="forbid")
19
+
20
+
21
+ class Missing(StrEnum):
22
+ """Why a run does not have a binding, in words safe to show the model."""
23
+
24
+ NOT_CONNECTED = "not connected"
25
+ WAITING_FOR_APPROVAL = "waiting for approval"
26
+ DENIED = "denied"
27
+ UNAVAILABLE = "not available"
28
+
29
+
30
+ class PlanReason(StrEnum):
31
+ """Why a capability is not authorized. Connections add AccessReason codes."""
32
+
33
+ NO_GRANT = "no_grant"
34
+ GRANT_PENDING = "grant_pending"
35
+ GRANT_DENIED = "grant_denied"
36
+ CONNECTION_MISSING = "connection_missing"
37
+ CONNECTION_EXPIRED = "connection_expired"
38
+ CONNECTION_REVOKED = "connection_revoked"
39
+ CONNECTION_WORKSPACE_MISMATCH = "connection_workspace_mismatch"
40
+ CONNECTION_KIND_MISMATCH = "connection_kind_mismatch"
41
+ CONNECTION_BINDING_MISMATCH = "connection_binding_mismatch"
42
+ TOOLS_NOT_GRANTED = "tools_not_granted"
43
+ TOOLS_UNAVAILABLE = "tools_unavailable"
44
+ TOOL_RULES_BROKEN = "tool_rules_broken"
45
+
46
+
47
+ class ExecutionChoice(Frozen):
48
+ """The agent backend the workspace chose, and the login a CLI agent uses."""
49
+
50
+ backend: AgentBackend
51
+ label: str
52
+ ready: bool
53
+ problem: str
54
+ connection_id: int | None = None
55
+ credential_key: str | None = None
56
+ owner_id: int | None = None
57
+
58
+
59
+ class PlanLimits(Frozen):
60
+ max_turns: int
61
+ max_tool_calls: int
62
+ max_tool_result_chars: int
63
+ token_budget: int
64
+ timeout_seconds: int
65
+
66
+ def value(self, limit: RunLimit) -> int:
67
+ return int(getattr(self, limit))
68
+
69
+
70
+ class AgentPlan(Frozen):
71
+ limits: PlanLimits
72
+ # The only tool names the agent may call.
73
+ tools: tuple[str, ...]
74
+ subagents: int = 0
75
+
76
+
77
+ class SkillSourcePlan(Frozen):
78
+ """A skill from another repository, pinned to the commit the run gets."""
79
+
80
+ repository: str
81
+ commit_sha: str
82
+
83
+
84
+ class JobPlan(Frozen):
85
+ key: str
86
+ # A path in the run's repository, or in skill_source's when it is set.
87
+ skill_path: str
88
+ config_hash: str
89
+ skill_source: SkillSourcePlan | None = None
90
+
91
+ @property
92
+ def sandbox_skill_path(self) -> str:
93
+ """Where the agent reads the skill. Empty for written instructions."""
94
+ if self.skill_source is not None:
95
+ return f"{SANDBOX_SKILL_DIR}/{PurePosixPath(self.skill_path).name}"
96
+ return self.skill_path
97
+
98
+
99
+ class RepositoryPlan(Frozen):
100
+ full_name: str
101
+ config_sha: str
102
+
103
+
104
+ class RunnerPlan(Frozen):
105
+ image: str
106
+ network_policy: Literal["none"]
107
+ # Only runs that publish the agent's changes need them recorded.
108
+ collect_changes: bool
109
+
110
+
111
+ class PullRequestPlan(Frozen):
112
+ """The draft pull request's base branch and CI choice."""
113
+
114
+ base: str
115
+ run_ci: bool
116
+
117
+
118
+ class IssuesPlan(Frozen):
119
+ """How many issues the agent may propose, and the labels each gets."""
120
+
121
+ max: int
122
+ labels: tuple[str, ...]
123
+
124
+
125
+ class ModelPlan(Frozen):
126
+ connection_id: int
127
+ connection_name: str
128
+ provider: str
129
+ family: str
130
+ default_model: str
131
+ allowed_models: tuple[str, ...]
132
+
133
+
134
+ class CapabilityPlan(Frozen):
135
+ binding: str
136
+ kind: str
137
+ required: bool
138
+ requested_tools: tuple[str, ...]
139
+ connection_id: int | None
140
+ connection_name: str | None
141
+ effective_tools: tuple[str, ...]
142
+ authorized: bool
143
+ # PlanReason and AccessReason codes. Older runs may hold retired ones.
144
+ reasons: tuple[str, ...]
145
+ missing: Missing | None = None
146
+ # The approved scope and tool definitions of an external tool.
147
+ external: dict[str, Any] | None = None
148
+ # Copied in so a rule changed mid-run does not affect the run.
149
+ tool_rules: dict[str, dict[str, Any]] = {}
150
+
151
+
152
+ class ExecutionPlan(Frozen):
153
+ execution: ExecutionChoice
154
+ ready: bool
155
+ agent: AgentPlan
156
+ job: JobPlan
157
+ repository: RepositoryPlan
158
+ runner: RunnerPlan
159
+ pull_request: PullRequestPlan | None
160
+ issues: IssuesPlan | None
161
+ model: ModelPlan | None
162
+ capabilities: tuple[CapabilityPlan, ...]
163
+
164
+ def limit(self, limit: RunLimit) -> int:
165
+ return self.agent.limits.value(limit)
166
+
167
+ def capability(self, kind: str, binding: str) -> CapabilityPlan:
168
+ return next(
169
+ item
170
+ for item in self.capabilities
171
+ if item.kind == kind and item.binding == binding
172
+ )
173
+
174
+ def stored(self) -> dict[str, Any]:
175
+ """The JSON saved on the run."""
176
+ return self.model_dump(mode="json")