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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- 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)
|
secondshift/jobs/api.py
ADDED
|
@@ -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)
|
secondshift/jobs/apps.py
ADDED
|
@@ -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
|
+
]
|