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
secondshift/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SecondShift: run repository agent skills safely, on schedule."""
|
secondshift/__main__.py
ADDED
secondshift/cli.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""The `secondshift` command. It runs Django management commands.
|
|
2
|
+
|
|
3
|
+
Installed, it defaults to the local environment (`secondshift up`). A
|
|
4
|
+
checkout's manage.py defaults to dev.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from django.core.management import execute_from_command_line
|
|
10
|
+
|
|
11
|
+
from secondshift.config.app import select_settings
|
|
12
|
+
from secondshift.config.constants import Environment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(default_environment: Environment = Environment.LOCAL) -> None:
|
|
16
|
+
select_settings(default_environment)
|
|
17
|
+
execute_from_command_line(sys.argv)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Compose the public API from routers owned by each Django app."""
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest, HttpResponse
|
|
4
|
+
from ninja import NinjaAPI
|
|
5
|
+
|
|
6
|
+
from secondshift.workspaces.access import AccessDenied
|
|
7
|
+
|
|
8
|
+
api = NinjaAPI(title="SecondShift API", version="1.0")
|
|
9
|
+
for app in (
|
|
10
|
+
"core",
|
|
11
|
+
"jobs",
|
|
12
|
+
"runs",
|
|
13
|
+
"triggers",
|
|
14
|
+
"workspaces",
|
|
15
|
+
"repositories",
|
|
16
|
+
"connections",
|
|
17
|
+
):
|
|
18
|
+
api.add_router("", f"secondshift.{app}.api.router")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@api.exception_handler(AccessDenied)
|
|
22
|
+
def access_denied(request: HttpRequest, error: AccessDenied) -> HttpResponse:
|
|
23
|
+
return api.create_response(request, {"detail": str(error)}, status=403)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Server entry points for the web app, the model gateway and the runner.
|
|
2
|
+
|
|
3
|
+
Start an HTTP role with SECONDSHIFT_SERVICE set to web, gateway or runner:
|
|
4
|
+
|
|
5
|
+
gunicorn --config python:secondshift.config.gunicorn
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from django.core.asgi import get_asgi_application
|
|
11
|
+
from django.core.handlers.asgi import ASGIHandler
|
|
12
|
+
from django.core.handlers.wsgi import WSGIHandler
|
|
13
|
+
from django.core.wsgi import get_wsgi_application
|
|
14
|
+
|
|
15
|
+
from secondshift.config.constants import (
|
|
16
|
+
ENVIRONMENT_VAR,
|
|
17
|
+
RUNNER_SETTINGS,
|
|
18
|
+
SERVICE_VAR,
|
|
19
|
+
SETTINGS_MODULE_VAR,
|
|
20
|
+
Environment,
|
|
21
|
+
Service,
|
|
22
|
+
settings_module,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def select_settings(default: Environment) -> None:
|
|
27
|
+
"""Use SECONDSHIFT_ENV, or the default, to pick the settings module."""
|
|
28
|
+
environment = Environment(os.environ.setdefault(ENVIRONMENT_VAR, default))
|
|
29
|
+
os.environ.setdefault(SETTINGS_MODULE_VAR, settings_module(environment))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def configure(service: Service) -> None:
|
|
33
|
+
os.environ[SERVICE_VAR] = service
|
|
34
|
+
if service == Service.RUNNER:
|
|
35
|
+
# Always its own settings, so it never loads the control plane's keys.
|
|
36
|
+
os.environ[SETTINGS_MODULE_VAR] = RUNNER_SETTINGS
|
|
37
|
+
else:
|
|
38
|
+
select_settings(default=Environment.PRODUCTION)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def started(service: Service) -> None:
|
|
42
|
+
if service == Service.RUNNER:
|
|
43
|
+
# Imported here because it needs settings, which are chosen above.
|
|
44
|
+
from secondshift.runner.services import get_runner_service # noqa: PLC0415
|
|
45
|
+
|
|
46
|
+
get_runner_service().verify_configuration()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def wsgi(name: str = Service.WEB) -> WSGIHandler:
|
|
50
|
+
service = Service(name)
|
|
51
|
+
configure(service)
|
|
52
|
+
application = get_wsgi_application()
|
|
53
|
+
started(service)
|
|
54
|
+
return application
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def asgi(name: str = Service.WEB) -> ASGIHandler:
|
|
58
|
+
service = Service(name)
|
|
59
|
+
configure(service)
|
|
60
|
+
application = get_asgi_application()
|
|
61
|
+
started(service)
|
|
62
|
+
return application
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Names shared by the settings modules, entry points and the launcher.
|
|
2
|
+
|
|
3
|
+
The runner's settings must never import the control plane's settings, which
|
|
4
|
+
read its keys. Values both need live here instead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
# Git object formats supported by repository snapshots and execution plans.
|
|
11
|
+
COMMIT_SHA_PATTERN = r"[0-9a-f]{40}|[0-9a-f]{64}"
|
|
12
|
+
|
|
13
|
+
EXECUTE_RUN_WORKFLOW = "secondshift.execute_run"
|
|
14
|
+
SCHEDULED_JOB_WORKFLOW = "secondshift.scheduled_job"
|
|
15
|
+
RETRY_NOTIFICATION_WORKFLOW = "secondshift.retry_notification"
|
|
16
|
+
RETRY_PULL_REQUEST_WORKFLOW = "secondshift.retry_pull_request"
|
|
17
|
+
RETRY_ISSUES_WORKFLOW = "secondshift.retry_issues"
|
|
18
|
+
|
|
19
|
+
ENVIRONMENT_VAR = "SECONDSHIFT_ENV"
|
|
20
|
+
SETTINGS_MODULE_VAR = "DJANGO_SETTINGS_MODULE"
|
|
21
|
+
SERVICE_VAR = "SECONDSHIFT_SERVICE"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
GUNICORN_CONFIG = "python:secondshift.config.gunicorn"
|
|
25
|
+
|
|
26
|
+
# How agent containers reach services on the Docker host.
|
|
27
|
+
HOST_ALIAS = "host.docker.internal"
|
|
28
|
+
# `secondshift up` builds this tag from the installed package.
|
|
29
|
+
LOCAL_AGENT_TAG = "secondshift-agent:local"
|
|
30
|
+
LOCAL_GATEWAY_URL = "http://127.0.0.1:8001"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# The gateway's MCP endpoint for subscription agents. Deploy files spell it out too.
|
|
34
|
+
AGENT_BROKER_PATH = "gateway/agent/mcp"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def local_agent_broker_url(gateway_url: str) -> str:
|
|
38
|
+
"""The gateway's agent broker, as agent containers on this host reach it."""
|
|
39
|
+
port = urlsplit(gateway_url).port or 80
|
|
40
|
+
return f"http://{HOST_ALIAS}:{port}/{AGENT_BROKER_PATH}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ArtifactStorage(StrEnum):
|
|
44
|
+
FILESYSTEM = "filesystem"
|
|
45
|
+
S3 = "s3"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AppEnv(StrEnum):
|
|
49
|
+
AGENT_IMAGE = "SECONDSHIFT_AGENT_IMAGE"
|
|
50
|
+
AGENT_RESOLV_CONF = "SECONDSHIFT_AGENT_RESOLV_CONF"
|
|
51
|
+
AGENT_NETWORK = "SECONDSHIFT_AGENT_NETWORK"
|
|
52
|
+
AGENT_BROKER_URL = "SECONDSHIFT_AGENT_BROKER_URL"
|
|
53
|
+
DATA_DIR = "SECONDSHIFT_DATA_DIR"
|
|
54
|
+
RUNNER_RUNTIME = "SECONDSHIFT_RUNNER_RUNTIME"
|
|
55
|
+
RUNNER_IMAGE = "SECONDSHIFT_RUNNER_IMAGE"
|
|
56
|
+
RUNNER_SERVICE_URL = "SECONDSHIFT_RUNNER_SERVICE_URL"
|
|
57
|
+
RUNNER_SERVICE_TOKEN = "SECONDSHIFT_RUNNER_SERVICE_TOKEN"
|
|
58
|
+
MODEL_GATEWAY_URL = "SECONDSHIFT_MODEL_GATEWAY_URL"
|
|
59
|
+
RUNNER_WORK_ROOT = "SECONDSHIFT_RUNNER_WORK_ROOT"
|
|
60
|
+
SOURCE_CACHE_ROOT = "SECONDSHIFT_SOURCE_CACHE_ROOT"
|
|
61
|
+
SOURCE_CACHE_MAX_BYTES = "SECONDSHIFT_SOURCE_CACHE_MAX_BYTES"
|
|
62
|
+
EXTERNAL_SKILLS_ENABLED = "SECONDSHIFT_EXTERNAL_SKILLS_ENABLED"
|
|
63
|
+
ARTIFACT_STORAGE = "SECONDSHIFT_ARTIFACT_STORAGE"
|
|
64
|
+
RUN_OUTPUT_MAX_BYTES = "SECONDSHIFT_RUN_OUTPUT_MAX_BYTES"
|
|
65
|
+
RUN_RETENTION_DAYS = "SECONDSHIFT_RUN_RETENTION_DAYS"
|
|
66
|
+
REPOSITORY_SYNC_INTERVAL_SECONDS = "SECONDSHIFT_REPOSITORY_SYNC_INTERVAL_SECONDS"
|
|
67
|
+
|
|
68
|
+
SECRET_KEY = "SECONDSHIFT_SECRET_KEY"
|
|
69
|
+
ENCRYPTION_KEY = "SECONDSHIFT_ENCRYPTION_KEY"
|
|
70
|
+
DATABASE_URL = "SECONDSHIFT_DATABASE_URL"
|
|
71
|
+
CACHE_URL = "SECONDSHIFT_CACHE_URL"
|
|
72
|
+
PUBLIC_URL = "SECONDSHIFT_PUBLIC_URL"
|
|
73
|
+
DEBUG = "SECONDSHIFT_DEBUG"
|
|
74
|
+
ALLOWED_HOSTS = "SECONDSHIFT_ALLOWED_HOSTS"
|
|
75
|
+
CSRF_TRUSTED_ORIGINS = "SECONDSHIFT_CSRF_TRUSTED_ORIGINS"
|
|
76
|
+
LOG_LEVEL = "SECONDSHIFT_LOG_LEVEL"
|
|
77
|
+
DB_CONN_MAX_AGE_SECONDS = "SECONDSHIFT_DB_CONN_MAX_AGE_SECONDS"
|
|
78
|
+
WORKFLOW_DATABASE_URL = "SECONDSHIFT_WORKFLOW_DATABASE_URL"
|
|
79
|
+
RUN_CONCURRENCY = "SECONDSHIFT_RUN_CONCURRENCY"
|
|
80
|
+
RUN_GLOBAL_CONCURRENCY = "SECONDSHIFT_RUN_GLOBAL_CONCURRENCY"
|
|
81
|
+
TASK_QUEUE_LIMIT = "SECONDSHIFT_TASK_QUEUE_LIMIT"
|
|
82
|
+
RUNNER_ALLOWED_HOSTS = "SECONDSHIFT_RUNNER_ALLOWED_HOSTS"
|
|
83
|
+
RUNNER_DATABASE_URL = "SECONDSHIFT_RUNNER_DATABASE_URL"
|
|
84
|
+
RUNNER_DB_CONN_MAX_AGE_SECONDS = "SECONDSHIFT_RUNNER_DB_CONN_MAX_AGE_SECONDS"
|
|
85
|
+
EMAIL_URL = "SECONDSHIFT_EMAIL_URL"
|
|
86
|
+
FROM_EMAIL = "SECONDSHIFT_FROM_EMAIL"
|
|
87
|
+
|
|
88
|
+
HTTP_BIND = "SECONDSHIFT_HTTP_BIND"
|
|
89
|
+
HTTP_PROCESSES = "SECONDSHIFT_HTTP_PROCESSES"
|
|
90
|
+
HTTP_TIMEOUT_SECONDS = "SECONDSHIFT_HTTP_TIMEOUT_SECONDS"
|
|
91
|
+
HTTP_GRACEFUL_TIMEOUT_SECONDS = "SECONDSHIFT_HTTP_GRACEFUL_TIMEOUT_SECONDS"
|
|
92
|
+
HTTP_LOG_LEVEL = "SECONDSHIFT_HTTP_LOG_LEVEL"
|
|
93
|
+
HTTP_ACCESS_LOG = "SECONDSHIFT_HTTP_ACCESS_LOG"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
GITHUB_HOST = "github.com"
|
|
97
|
+
GITHUB_BASE_URL = f"https://{GITHUB_HOST}"
|
|
98
|
+
GITHUB_SSH_PREFIX = f"git@{GITHUB_HOST}:"
|
|
99
|
+
GITHUB_REPOSITORY_PATTERN = r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"
|
|
100
|
+
GITHUB_API_URL = "https://api.github.com"
|
|
101
|
+
GITHUB_OAUTH_URL = f"{GITHUB_BASE_URL}/login/oauth"
|
|
102
|
+
# The REST API version every request asks for.
|
|
103
|
+
GITHUB_API_VERSION = "2022-11-28"
|
|
104
|
+
# Fine-grained tokens are limited to chosen repositories. Classic tokens are not.
|
|
105
|
+
GITHUB_FINE_GRAINED_TOKEN_PATTERN = r"github_pat_[A-Za-z0-9_]+"
|
|
106
|
+
GITHUB_TOKEN_HINT = "Enter a fine-grained GitHub token. It starts with github_pat_."
|
|
107
|
+
# Limits for the files of one commit that a run gets. The control plane checks
|
|
108
|
+
# them while it builds a snapshot, and the runner checks them again.
|
|
109
|
+
MAX_SOURCE_SNAPSHOT_BYTES = 64 * 1024 * 1024
|
|
110
|
+
MAX_SOURCE_UNPACKED_BYTES = 1024 * 1024 * 1024
|
|
111
|
+
MAX_SOURCE_FILES = 200_000
|
|
112
|
+
# Limits for what a run leaves behind: the agent's file changes, and the files
|
|
113
|
+
# it saves in /outputs. The runner applies them, and the control plane again.
|
|
114
|
+
MAX_CHANGES_BYTES = 20 * 1024 * 1024
|
|
115
|
+
MAX_OUTPUT_FILES = 100
|
|
116
|
+
MAX_OUTPUT_BYTES = 100 * 1024 * 1024
|
|
117
|
+
# Earlier runs' saved files that one run gets back. Uploaded like a snapshot,
|
|
118
|
+
# so it stays under that limit even when the files do not compress.
|
|
119
|
+
MAX_HISTORY_FILES_BYTES = 48 * 1024 * 1024
|
|
120
|
+
# Issues one run may open. A weekly review should file a few, not dozens.
|
|
121
|
+
MAX_RUN_ISSUES = 10
|
|
122
|
+
# Where a sandbox finds the folder for files it wants to hand back.
|
|
123
|
+
SANDBOX_OUTPUTS_DIR = "/outputs"
|
|
124
|
+
# Where a sandbox finds the files its job's earlier runs saved. Read-only.
|
|
125
|
+
SANDBOX_HISTORY_DIR = "/history"
|
|
126
|
+
# Where a sandbox finds a skill from another repository. Read-only.
|
|
127
|
+
SANDBOX_SKILL_DIR = "/skill"
|
|
128
|
+
# Limits for a skill from another repository: its SKILL.md and the files beside it.
|
|
129
|
+
MAX_SKILL_FILES = 500
|
|
130
|
+
MAX_SKILL_UNPACKED_BYTES = 20 * 1024 * 1024
|
|
131
|
+
MAX_SKILL_SNAPSHOT_BYTES = 10 * 1024 * 1024
|
|
132
|
+
GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/readonly"
|
|
133
|
+
# The model gateway refuses a run past its token budget with this message.
|
|
134
|
+
TOKEN_BUDGET_MESSAGE = "This run has used its token budget."
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TriggerKind(StrEnum):
|
|
138
|
+
"""Events a repository may ask to start a job. Never a URL or a secret."""
|
|
139
|
+
|
|
140
|
+
WEBHOOK = "webhook"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ModelFamily(StrEnum):
|
|
144
|
+
"""The API shape a model provider speaks. Names the gateway's URL path."""
|
|
145
|
+
|
|
146
|
+
ANTHROPIC = "anthropic"
|
|
147
|
+
OPENAI = "openai"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class RunStart(StrEnum):
|
|
151
|
+
"""What started a run."""
|
|
152
|
+
|
|
153
|
+
SCHEDULE = "schedule"
|
|
154
|
+
MANUAL = "manual"
|
|
155
|
+
WEBHOOK = "webhook"
|
|
156
|
+
MCP = "mcp"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# How much of a report a run keeps as its summary. Webhooks send the summary.
|
|
160
|
+
SUMMARY_MAX_CHARS = 4_000
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class GitHubMCPTool(StrEnum):
|
|
164
|
+
"""Read tools for pull requests, commits, and Actions runs.
|
|
165
|
+
|
|
166
|
+
Actions tools that trigger runs, list artifacts, or return download links
|
|
167
|
+
are left out or blocked by method.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
SEARCH_PULL_REQUESTS = "search_pull_requests"
|
|
171
|
+
PULL_REQUEST_READ = "pull_request_read"
|
|
172
|
+
LIST_COMMITS = "list_commits"
|
|
173
|
+
GET_COMMIT = "get_commit"
|
|
174
|
+
ACTIONS_LIST = "actions_list"
|
|
175
|
+
ACTIONS_GET = "actions_get"
|
|
176
|
+
GET_JOB_LOGS = "get_job_logs"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
GITHUB_MCP_TOOLS = tuple(GitHubMCPTool)
|
|
180
|
+
|
|
181
|
+
SENTRY_MCP_URL = "https://mcp.sentry.dev/mcp"
|
|
182
|
+
# Sentry organization and project slugs.
|
|
183
|
+
SENTRY_SLUG_PATTERN = r"[a-z0-9][a-z0-9_\-]{0,63}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class SentryMCPTool(StrEnum):
|
|
187
|
+
"""Read tools for errors, issues, and traces.
|
|
188
|
+
|
|
189
|
+
The search tools pass their query to Sentry's own search agent. Replays,
|
|
190
|
+
logs, and profiles are left out because they often hold personal data.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
FIND_PROJECTS = "find_projects"
|
|
194
|
+
SEARCH_ISSUES = "search_issues"
|
|
195
|
+
SEARCH_ERRORS = "search_errors"
|
|
196
|
+
SEARCH_TRACES = "search_traces"
|
|
197
|
+
GET_SENTRY_RESOURCE = "get_sentry_resource"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
SENTRY_MCP_TOOLS = tuple(SentryMCPTool)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Environment(StrEnum):
|
|
204
|
+
"""Where SecondShift runs. Each one has a settings module of the same name."""
|
|
205
|
+
|
|
206
|
+
PRODUCTION = "production"
|
|
207
|
+
LOCAL = "local"
|
|
208
|
+
DEV = "dev"
|
|
209
|
+
TEST = "test"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Service(StrEnum):
|
|
213
|
+
"""The separately started parts of SecondShift."""
|
|
214
|
+
|
|
215
|
+
WEB = "web"
|
|
216
|
+
GATEWAY = "gateway"
|
|
217
|
+
RUNNER = "runner"
|
|
218
|
+
WORKER = "worker"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def settings_module(name: str) -> str:
|
|
222
|
+
return f"secondshift.config.settings.{name}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SandboxRuntime(StrEnum):
|
|
226
|
+
"""The Docker runtime that runs command sandboxes."""
|
|
227
|
+
|
|
228
|
+
DOCKER = "docker"
|
|
229
|
+
GVISOR = "runsc"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class RunSandbox(StrEnum):
|
|
233
|
+
"""What a run records as its sandbox."""
|
|
234
|
+
|
|
235
|
+
PENDING = "pending-runner-service"
|
|
236
|
+
ISOLATED_RUNNER = "isolated-runner"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
RUNNER_SETTINGS = settings_module(Service.RUNNER)
|
|
240
|
+
|
|
241
|
+
# Docker's official Python image, pinned by digest so a rebuilt tag can't change
|
|
242
|
+
# what runs. The agent's tools need only `find`, `cat` and `grep`. Git runs in
|
|
243
|
+
# the runner, not the sandbox. Operators override it with SECONDSHIFT_RUNNER_IMAGE.
|
|
244
|
+
# To update, run `docker buildx imagetools inspect python:3.13-slim` and copy
|
|
245
|
+
# the index digest.
|
|
246
|
+
DEFAULT_SANDBOX_IMAGE = (
|
|
247
|
+
"python:3.13-slim"
|
|
248
|
+
"@sha256:57b29a7c050c4e679e4bea572e14b3965cf4b5839de2da49d55e49ffc4024f39"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# The repository file that lists jobs, at the repository root.
|
|
252
|
+
MANIFEST_FILE = ".secondshift.yaml"
|
|
253
|
+
|
|
254
|
+
# How much of a skill file a job keeps for people to read in the dashboard.
|
|
255
|
+
MAX_SKILL_PREVIEW_CHARS = 200_000
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class AgentBackend(StrEnum):
|
|
259
|
+
"""Who runs the agent: SecondShift's own loop, or a subscription CLI."""
|
|
260
|
+
|
|
261
|
+
PYDANTIC = "pydantic"
|
|
262
|
+
CLAUDE_CODE = "claude_code"
|
|
263
|
+
CODEX = "codex"
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def uses_model_api(self) -> bool:
|
|
267
|
+
"""Runs call a model provider through SecondShift's gateway. The CLIs
|
|
268
|
+
call their provider directly with the subscription login."""
|
|
269
|
+
return self is AgentBackend.PYDANTIC
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def starts_subagents(self) -> bool:
|
|
273
|
+
"""Runs can start sub-agents when the job allows them."""
|
|
274
|
+
return self is not AgentBackend.CODEX
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def limits_model_calls(self) -> bool:
|
|
278
|
+
"""Runs stop at the run's model-call limit."""
|
|
279
|
+
return self is not AgentBackend.CODEX
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Generate private per-role env files for a new container installation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import secrets
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from cryptography.fernet import Fernet
|
|
10
|
+
|
|
11
|
+
from secondshift.config.constants import (
|
|
12
|
+
ENVIRONMENT_VAR,
|
|
13
|
+
SERVICE_VAR,
|
|
14
|
+
AppEnv,
|
|
15
|
+
Environment,
|
|
16
|
+
SandboxRuntime,
|
|
17
|
+
Service,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ROLE_FILES = {role: f"{role}.env" for role in Service}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_environment(hostname: str, directory: Path) -> None:
|
|
24
|
+
if len(hostname) > 253 or not all(
|
|
25
|
+
re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?", label)
|
|
26
|
+
for label in hostname.split(".")
|
|
27
|
+
):
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"Use a DNS hostname without a scheme, port, path or credentials"
|
|
30
|
+
)
|
|
31
|
+
directory = directory.resolve()
|
|
32
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
# Never overwrite existing configuration or replace its keys.
|
|
34
|
+
if any((directory / name).exists() for name in [*ROLE_FILES.values(), ".env"]):
|
|
35
|
+
raise FileExistsError(
|
|
36
|
+
"Configuration already exists; reuse it rather than generating new keys"
|
|
37
|
+
)
|
|
38
|
+
common = {
|
|
39
|
+
AppEnv.SECRET_KEY: secrets.token_urlsafe(48),
|
|
40
|
+
AppEnv.ENCRYPTION_KEY: Fernet.generate_key().decode(),
|
|
41
|
+
AppEnv.DATABASE_URL: "sqlite:////var/lib/secondshift/control.sqlite3",
|
|
42
|
+
}
|
|
43
|
+
public = {
|
|
44
|
+
AppEnv.PUBLIC_URL: f"https://{hostname.lower()}",
|
|
45
|
+
AppEnv.RUNNER_RUNTIME: SandboxRuntime.GVISOR,
|
|
46
|
+
}
|
|
47
|
+
token = secrets.token_urlsafe(48)
|
|
48
|
+
runner = directory / "runner"
|
|
49
|
+
runner.mkdir(mode=0o700, exist_ok=True)
|
|
50
|
+
configs = {
|
|
51
|
+
Service.WEB: {
|
|
52
|
+
**common,
|
|
53
|
+
**public,
|
|
54
|
+
AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
|
|
55
|
+
AppEnv.RUNNER_SERVICE_TOKEN: token,
|
|
56
|
+
AppEnv.HTTP_TIMEOUT_SECONDS: "300",
|
|
57
|
+
},
|
|
58
|
+
Service.GATEWAY: {
|
|
59
|
+
**common,
|
|
60
|
+
# The agent broker runs workspace tools for subscription runs.
|
|
61
|
+
AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
|
|
62
|
+
AppEnv.RUNNER_SERVICE_TOKEN: token,
|
|
63
|
+
AppEnv.ALLOWED_HOSTS: "secondshift-gateway",
|
|
64
|
+
AppEnv.HTTP_BIND: "0.0.0.0:8001",
|
|
65
|
+
AppEnv.HTTP_GRACEFUL_TIMEOUT_SECONDS: "600",
|
|
66
|
+
},
|
|
67
|
+
Service.WORKER: {
|
|
68
|
+
**common,
|
|
69
|
+
**public,
|
|
70
|
+
AppEnv.RUNNER_SERVICE_URL: "http://secondshift-runner:8002",
|
|
71
|
+
AppEnv.RUNNER_SERVICE_TOKEN: token,
|
|
72
|
+
AppEnv.MODEL_GATEWAY_URL: "http://secondshift-gateway:8001",
|
|
73
|
+
},
|
|
74
|
+
Service.RUNNER: {
|
|
75
|
+
AppEnv.HTTP_BIND: "0.0.0.0:8002",
|
|
76
|
+
AppEnv.HTTP_PROCESSES: "1",
|
|
77
|
+
AppEnv.HTTP_TIMEOUT_SECONDS: "180",
|
|
78
|
+
AppEnv.RUNNER_ALLOWED_HOSTS: "secondshift-runner",
|
|
79
|
+
AppEnv.RUNNER_SERVICE_TOKEN: token,
|
|
80
|
+
AppEnv.RUNNER_RUNTIME: SandboxRuntime.GVISOR,
|
|
81
|
+
AppEnv.DATA_DIR: str(runner),
|
|
82
|
+
AppEnv.RUNNER_WORK_ROOT: str(runner),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
for role, values in configs.items():
|
|
86
|
+
descriptor = os.open(
|
|
87
|
+
directory / ROLE_FILES[role], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
|
|
88
|
+
)
|
|
89
|
+
with os.fdopen(descriptor, "w") as output:
|
|
90
|
+
output.write(f"{ENVIRONMENT_VAR}={Environment.PRODUCTION}\n")
|
|
91
|
+
output.write(f"{SERVICE_VAR}={role}\n")
|
|
92
|
+
for name, value in values.items():
|
|
93
|
+
output.write(f"{name}={value}\n")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> None:
|
|
97
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--hostname", required=True, help="Public DNS hostname, without https://"
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--directory",
|
|
103
|
+
type=Path,
|
|
104
|
+
default=Path.cwd(),
|
|
105
|
+
help="Output directory (default: current directory)",
|
|
106
|
+
)
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
try:
|
|
109
|
+
generate_environment(args.hostname, args.directory)
|
|
110
|
+
except (ValueError, OSError) as error:
|
|
111
|
+
parser.exit(1, f"{error}\n")
|
|
112
|
+
print(
|
|
113
|
+
"Created web.env, gateway.env, worker.env and runner.env. Keep these files private."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""First start of a development install: create the data folder and its keys."""
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from cryptography.fernet import Fernet
|
|
9
|
+
|
|
10
|
+
from secondshift.config.constants import AppEnv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_generated_env(data_dir: Path) -> None:
|
|
14
|
+
"""Create the data folder with fresh keys in its .env, once.
|
|
15
|
+
|
|
16
|
+
The runner gets only its token, in runner.env. A lock keeps processes
|
|
17
|
+
starting together from writing different keys.
|
|
18
|
+
"""
|
|
19
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
20
|
+
with open(data_dir / ".lock", "w") as lock:
|
|
21
|
+
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
22
|
+
if (data_dir / ".env").exists():
|
|
23
|
+
return
|
|
24
|
+
token = secrets.token_urlsafe(48)
|
|
25
|
+
files = {
|
|
26
|
+
"runner.env": f"SECONDSHIFT_RUNNER_SERVICE_TOKEN={token}\n",
|
|
27
|
+
".env": (
|
|
28
|
+
"# Generated on first start. Back it up: SECONDSHIFT_ENCRYPTION_KEY\n"
|
|
29
|
+
"# decrypts stored connection credentials.\n"
|
|
30
|
+
f"{AppEnv.SECRET_KEY}={secrets.token_urlsafe(48)}\n"
|
|
31
|
+
f"{AppEnv.ENCRYPTION_KEY}={Fernet.generate_key().decode()}\n"
|
|
32
|
+
f"SECONDSHIFT_RUNNER_SERVICE_TOKEN={token}\n"
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
for name, content in files.items(): # .env last: it marks completion
|
|
36
|
+
descriptor = os.open(
|
|
37
|
+
data_dir / name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600
|
|
38
|
+
)
|
|
39
|
+
with os.fdopen(descriptor, "w") as file:
|
|
40
|
+
file.write(content)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared Gunicorn options, configured by each process environment."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from environ import Env
|
|
6
|
+
|
|
7
|
+
from secondshift.config.constants import SERVICE_VAR, AppEnv, Service
|
|
8
|
+
|
|
9
|
+
service = Service(os.environ.get(SERVICE_VAR, Service.WEB))
|
|
10
|
+
if service == Service.WORKER:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
"The worker role runs python manage.py run_dbos_worker, not Gunicorn"
|
|
13
|
+
)
|
|
14
|
+
env = Env()
|
|
15
|
+
|
|
16
|
+
# Both hold long requests open (model streams, agent runs) while serving others.
|
|
17
|
+
async_http = service in (Service.GATEWAY, Service.RUNNER)
|
|
18
|
+
factory = "asgi" if async_http else "wsgi"
|
|
19
|
+
wsgi_app = f"secondshift.config.app:{factory}('{service}')"
|
|
20
|
+
worker_class = "uvicorn_worker.UvicornWorker" if async_http else "sync"
|
|
21
|
+
# One address, or several separated by commas.
|
|
22
|
+
bind = env.list(AppEnv.HTTP_BIND, default=["0.0.0.0:8000"])
|
|
23
|
+
workers = env.int(AppEnv.HTTP_PROCESSES, default=2)
|
|
24
|
+
timeout = env.int(AppEnv.HTTP_TIMEOUT_SECONDS, default=30)
|
|
25
|
+
graceful_timeout = env.int(AppEnv.HTTP_GRACEFUL_TIMEOUT_SECONDS, default=30)
|
|
26
|
+
loglevel = env.str(AppEnv.HTTP_LOG_LEVEL, default="info").lower()
|
|
27
|
+
accesslog = env.str(AppEnv.HTTP_ACCESS_LOG, default="-") or None
|
|
28
|
+
if workers < 1 or timeout < 1 or graceful_timeout < 1:
|
|
29
|
+
raise ValueError("Gunicorn worker count and timeouts must be positive")
|
|
30
|
+
# Services may share a data directory. Never compete for a shared control socket.
|
|
31
|
+
control_socket_disable = True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Shared logging policy for the control plane and isolated runner."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def logging_config(level: str, service: str) -> dict[str, Any]:
|
|
7
|
+
"""Route application and Django records through one console handler."""
|
|
8
|
+
return {
|
|
9
|
+
"version": 1,
|
|
10
|
+
"disable_existing_loggers": False,
|
|
11
|
+
"formatters": {
|
|
12
|
+
"console": {
|
|
13
|
+
"format": (
|
|
14
|
+
"{asctime} {levelname} " + service + "[{process}] {name}: {message}"
|
|
15
|
+
),
|
|
16
|
+
"style": "{",
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"handlers": {
|
|
20
|
+
"console": {"class": "logging.StreamHandler", "formatter": "console"},
|
|
21
|
+
"null": {"class": "logging.NullHandler"},
|
|
22
|
+
},
|
|
23
|
+
"root": {"handlers": ["console"], "level": level},
|
|
24
|
+
"loggers": {
|
|
25
|
+
# Replace Django's default handlers to avoid duplicate output and
|
|
26
|
+
# let LOG_LEVEL apply to requests and development server messages.
|
|
27
|
+
"django": {"handlers": [], "level": "NOTSET", "propagate": True},
|
|
28
|
+
"django.server": {
|
|
29
|
+
"handlers": [],
|
|
30
|
+
"level": "NOTSET",
|
|
31
|
+
"propagate": True,
|
|
32
|
+
},
|
|
33
|
+
# Request URLs may contain provider or webhook credentials.
|
|
34
|
+
"httpx": {"level": "WARNING"},
|
|
35
|
+
"httpcore": {"level": "WARNING"},
|
|
36
|
+
# The SDK can log raw payloads and session identifiers. The broker
|
|
37
|
+
# emits bounded audit events and sanitized errors instead.
|
|
38
|
+
"mcp": {"handlers": ["null"], "propagate": False},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
File without changes
|