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,524 @@
|
|
|
1
|
+
"""Settings shared by every environment.
|
|
2
|
+
|
|
3
|
+
SECONDSHIFT_ENV picks the settings module. It is read from the process
|
|
4
|
+
environment, never from .env.
|
|
5
|
+
|
|
6
|
+
- production: a server. production.py requires every key and adds HTTPS.
|
|
7
|
+
- local: `secondshift up` on one machine for one person (local.py).
|
|
8
|
+
- dev: a checkout with the Vite dev server (dev.py).
|
|
9
|
+
- test: the test suite (test.py).
|
|
10
|
+
|
|
11
|
+
Defaults are safe for production. Values that differ on a single machine use
|
|
12
|
+
`default=SINGLE_HOST` or `default=DEV`.
|
|
13
|
+
|
|
14
|
+
Local mode must work with no settings beyond the keys made on first start. The
|
|
15
|
+
data folder's .env outlives upgrades, so local values belong in code.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from datetime import timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from urllib.parse import urlsplit
|
|
22
|
+
|
|
23
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
24
|
+
from django.utils.csp import CSP
|
|
25
|
+
from environ import Env
|
|
26
|
+
|
|
27
|
+
from secondshift.config.constants import (
|
|
28
|
+
DEFAULT_SANDBOX_IMAGE,
|
|
29
|
+
ENVIRONMENT_VAR,
|
|
30
|
+
GITHUB_BASE_URL,
|
|
31
|
+
LOCAL_AGENT_TAG,
|
|
32
|
+
LOCAL_GATEWAY_URL,
|
|
33
|
+
SERVICE_VAR,
|
|
34
|
+
AppEnv,
|
|
35
|
+
ArtifactStorage,
|
|
36
|
+
Environment,
|
|
37
|
+
SandboxRuntime,
|
|
38
|
+
Service,
|
|
39
|
+
local_agent_broker_url,
|
|
40
|
+
)
|
|
41
|
+
from secondshift.config.first_run import write_generated_env
|
|
42
|
+
from secondshift.config.logging import logging_config
|
|
43
|
+
from secondshift.config.static_storage import immutable_static_file
|
|
44
|
+
|
|
45
|
+
# The secondshift package, and the checkout that contains it.
|
|
46
|
+
PACKAGE_DIR = Path(__file__).resolve().parents[2]
|
|
47
|
+
BASE_DIR = PACKAGE_DIR.parent
|
|
48
|
+
# The built dashboard (npm --prefix dashboard run build).
|
|
49
|
+
WEB_DIR = PACKAGE_DIR / "web"
|
|
50
|
+
|
|
51
|
+
ENVIRONMENT = Environment(os.environ.get(ENVIRONMENT_VAR, Environment.PRODUCTION))
|
|
52
|
+
DEV = ENVIRONMENT == Environment.DEV
|
|
53
|
+
# One trusted person on one machine. Plain Docker, local repositories and
|
|
54
|
+
# generated keys are fine there.
|
|
55
|
+
SINGLE_HOST = ENVIRONMENT in {Environment.DEV, Environment.LOCAL}
|
|
56
|
+
|
|
57
|
+
env = Env()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Real environment wins, then a checkout's .env, then the data folder's .env.
|
|
61
|
+
# A checkout's .env holds development settings. Local mode skips it so it runs
|
|
62
|
+
# the same as an installed copy. Tests skip it so they use the defaults.
|
|
63
|
+
if ENVIRONMENT not in {Environment.LOCAL, Environment.TEST}:
|
|
64
|
+
Env.read_env(BASE_DIR / ".env")
|
|
65
|
+
# Everything a single-host install writes: database, run files and runner
|
|
66
|
+
# workspaces. On a single host its .env also holds keys made on first start.
|
|
67
|
+
DATA_DIR = Path(env.str(AppEnv.DATA_DIR, default="~/.secondshift")).expanduser()
|
|
68
|
+
if SINGLE_HOST:
|
|
69
|
+
write_generated_env(DATA_DIR)
|
|
70
|
+
Env.read_env(DATA_DIR / ".env")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def sqlalchemy_url(database_url: str) -> str:
|
|
74
|
+
"""DBOS uses SQLAlchemy, which needs the psycopg driver named explicitly."""
|
|
75
|
+
for scheme in ("postgres://", "postgresql://"):
|
|
76
|
+
if database_url.startswith(scheme):
|
|
77
|
+
return "postgresql+psycopg://" + database_url.removeprefix(scheme)
|
|
78
|
+
return database_url
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_limit(
|
|
82
|
+
setting: str, default: int, ceiling: int, minimum: int
|
|
83
|
+
) -> tuple[int, int]:
|
|
84
|
+
"""A run limit's default and the highest value an owner may choose.
|
|
85
|
+
|
|
86
|
+
Read from SECONDSHIFT_<setting> and SECONDSHIFT_<setting>_CEILING, the
|
|
87
|
+
names workspaces.limits.LimitSpec shows.
|
|
88
|
+
"""
|
|
89
|
+
name = f"SECONDSHIFT_{setting}"
|
|
90
|
+
value = env.int(name, default=default)
|
|
91
|
+
highest = env.int(f"{name}_CEILING", default=max(ceiling, value))
|
|
92
|
+
if value < minimum:
|
|
93
|
+
raise ImproperlyConfigured(
|
|
94
|
+
f"{name} must be positive" if minimum else f"{name} cannot be negative"
|
|
95
|
+
)
|
|
96
|
+
if highest < value:
|
|
97
|
+
raise ImproperlyConfigured(f"{name}_CEILING cannot be below {name}")
|
|
98
|
+
return value, highest
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- Core --------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
# Required in production. Generated into DATA_DIR/.env on a single host.
|
|
104
|
+
SECRET_KEY = env.str(AppEnv.SECRET_KEY, default="")
|
|
105
|
+
FIELD_ENCRYPTION_KEY = env.str(AppEnv.ENCRYPTION_KEY, default="")
|
|
106
|
+
# Off unless asked for, on by default in development. production.py refuses it.
|
|
107
|
+
DEBUG = env.bool(AppEnv.DEBUG, default=DEV)
|
|
108
|
+
FRONTEND_URL = env.str(AppEnv.PUBLIC_URL, default="http://localhost:5173")
|
|
109
|
+
try:
|
|
110
|
+
PUBLIC_HOST = urlsplit(FRONTEND_URL).hostname
|
|
111
|
+
except ValueError:
|
|
112
|
+
PUBLIC_HOST = None
|
|
113
|
+
ALLOWED_HOSTS = env.list(
|
|
114
|
+
AppEnv.ALLOWED_HOSTS,
|
|
115
|
+
default=["localhost", "127.0.0.1"] if SINGLE_HOST else [PUBLIC_HOST or "localhost"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
INSTALLED_APPS = [
|
|
119
|
+
"django.contrib.admin",
|
|
120
|
+
"django.contrib.auth",
|
|
121
|
+
"django.contrib.contenttypes",
|
|
122
|
+
"django.contrib.sessions",
|
|
123
|
+
"django.contrib.messages",
|
|
124
|
+
"django.contrib.staticfiles",
|
|
125
|
+
"django.contrib.sites",
|
|
126
|
+
"corsheaders",
|
|
127
|
+
"allauth",
|
|
128
|
+
"allauth.account",
|
|
129
|
+
"allauth.socialaccount",
|
|
130
|
+
"allauth.socialaccount.providers.github",
|
|
131
|
+
"axes",
|
|
132
|
+
"health_check",
|
|
133
|
+
"secondshift.core",
|
|
134
|
+
"secondshift.workspaces",
|
|
135
|
+
"secondshift.connections",
|
|
136
|
+
"secondshift.repositories",
|
|
137
|
+
"secondshift.jobs",
|
|
138
|
+
"secondshift.runs",
|
|
139
|
+
"secondshift.triggers",
|
|
140
|
+
"secondshift.gateway",
|
|
141
|
+
"secondshift.mcp_server",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
MIDDLEWARE = [
|
|
145
|
+
"secondshift.core.middleware.LivenessMiddleware",
|
|
146
|
+
"django.middleware.security.SecurityMiddleware",
|
|
147
|
+
"django.middleware.csp.ContentSecurityPolicyMiddleware",
|
|
148
|
+
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
149
|
+
"corsheaders.middleware.CorsMiddleware",
|
|
150
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
151
|
+
"django.middleware.common.CommonMiddleware",
|
|
152
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
153
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
154
|
+
"allauth.account.middleware.AccountMiddleware",
|
|
155
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
156
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
157
|
+
"axes.middleware.AxesMiddleware",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Set by secondshift.config.app. The gateway serves model routes and run-scoped
|
|
161
|
+
# agent tools to trusted orchestrators, never the offline command sandbox.
|
|
162
|
+
SERVICE = Service(env.str(SERVICE_VAR, default=Service.WEB))
|
|
163
|
+
ROOT_URLCONF = (
|
|
164
|
+
"secondshift.gateway.urls"
|
|
165
|
+
if SERVICE == Service.GATEWAY
|
|
166
|
+
else "secondshift.config.urls"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
TEMPLATES = [
|
|
170
|
+
{
|
|
171
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
172
|
+
"DIRS": [PACKAGE_DIR / "templates"],
|
|
173
|
+
"APP_DIRS": True,
|
|
174
|
+
"OPTIONS": {
|
|
175
|
+
"context_processors": [
|
|
176
|
+
"django.template.context_processors.request",
|
|
177
|
+
"django.contrib.auth.context_processors.auth",
|
|
178
|
+
"django.contrib.messages.context_processors.messages",
|
|
179
|
+
"secondshift.workspaces.context_processors.invite",
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
LANGUAGE_CODE = "en-us"
|
|
186
|
+
TIME_ZONE = "UTC"
|
|
187
|
+
USE_I18N = True
|
|
188
|
+
USE_TZ = True
|
|
189
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
190
|
+
SITE_ID = 1
|
|
191
|
+
|
|
192
|
+
LOGGING = logging_config(env.str(AppEnv.LOG_LEVEL, default="INFO").upper(), SERVICE)
|
|
193
|
+
|
|
194
|
+
# --- Database and durable workflows ------------------------------------------
|
|
195
|
+
|
|
196
|
+
DATABASE_URL = env.str(
|
|
197
|
+
AppEnv.DATABASE_URL, default=f"sqlite:///{DATA_DIR / 'secondshift.sqlite3'}"
|
|
198
|
+
)
|
|
199
|
+
DATABASES = {
|
|
200
|
+
"default": {
|
|
201
|
+
**env.db_url_config(DATABASE_URL),
|
|
202
|
+
"CONN_MAX_AGE": env.int(AppEnv.DB_CONN_MAX_AGE_SECONDS, default=60),
|
|
203
|
+
"CONN_HEALTH_CHECKS": True,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
DEFAULT_WORKFLOW_DATABASE_URL = DATABASE_URL
|
|
207
|
+
if DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
|
|
208
|
+
# Dispatch holds an application write lock while DBOS writes through a
|
|
209
|
+
# separate connection. SQLite needs separate files to avoid self-deadlock.
|
|
210
|
+
DEFAULT_WORKFLOW_DATABASE_URL = f"sqlite:///{DATA_DIR / 'workflows.sqlite3'}"
|
|
211
|
+
# Web, gateway and worker share one file. WAL lets reads run during a
|
|
212
|
+
# write, IMMEDIATE transactions and a timeout make writers wait their turn
|
|
213
|
+
# instead of failing with "database is locked".
|
|
214
|
+
DATABASES["default"]["OPTIONS"] = {
|
|
215
|
+
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
|
|
216
|
+
"transaction_mode": "IMMEDIATE",
|
|
217
|
+
"timeout": 20,
|
|
218
|
+
}
|
|
219
|
+
# Sign-in rate limits and the health check use the cache. Every process must
|
|
220
|
+
# see the same counts, so the default is a table in the application database.
|
|
221
|
+
CACHES = {
|
|
222
|
+
"default": env.cache_url(AppEnv.CACHE_URL, default="dbcache://secondshift_cache")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
DBOS_SYSTEM_DATABASE_URL = sqlalchemy_url(
|
|
226
|
+
env.str(
|
|
227
|
+
AppEnv.WORKFLOW_DATABASE_URL,
|
|
228
|
+
default=DEFAULT_WORKFLOW_DATABASE_URL,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
DBOS_APPLICATION_NAME = "secondshift"
|
|
232
|
+
# Fixed, so a deploy still recovers workflows started by the previous version.
|
|
233
|
+
DBOS_APPLICATION_VERSION = "0.1.0"
|
|
234
|
+
DBOS_QUEUE_NAME = "agent-runs"
|
|
235
|
+
DBOS_WORKER_CONCURRENCY = env.int(AppEnv.RUN_CONCURRENCY, default=2)
|
|
236
|
+
DBOS_GLOBAL_CONCURRENCY = env.int(AppEnv.RUN_GLOBAL_CONCURRENCY, default=0)
|
|
237
|
+
TASK_QUEUE_LIMIT = env.int(AppEnv.TASK_QUEUE_LIMIT, default=100)
|
|
238
|
+
|
|
239
|
+
# --- Static files and run artifacts ------------------------------------------
|
|
240
|
+
|
|
241
|
+
STATIC_URL = "/static/"
|
|
242
|
+
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
243
|
+
WHITENOISE_IMMUTABLE_FILE_TEST = immutable_static_file
|
|
244
|
+
# The built dashboard. In a checkout also its source assets, so admin has its
|
|
245
|
+
# icon before the first build.
|
|
246
|
+
STATICFILES_DIRS = [
|
|
247
|
+
path for path in (WEB_DIR, BASE_DIR / "dashboard" / "public") if path.is_dir()
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
# Private run output, never served over HTTP. "filesystem" needs a persistent
|
|
251
|
+
# volume at SECONDSHIFT_ARTIFACT_ROOT in production, "s3" works with any
|
|
252
|
+
# S3-compatible service (AWS, R2, Hetzner, RustFS locally).
|
|
253
|
+
ARTIFACT_STORAGE = env.str(AppEnv.ARTIFACT_STORAGE, default=ArtifactStorage.FILESYSTEM)
|
|
254
|
+
ARTIFACT_STORAGES = {
|
|
255
|
+
ArtifactStorage.FILESYSTEM: {
|
|
256
|
+
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
257
|
+
"OPTIONS": {
|
|
258
|
+
"location": env.path(
|
|
259
|
+
"SECONDSHIFT_ARTIFACT_ROOT", default=DATA_DIR / "artifacts"
|
|
260
|
+
),
|
|
261
|
+
"base_url": None,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
ArtifactStorage.S3: {
|
|
265
|
+
"BACKEND": "storages.backends.s3.S3Storage",
|
|
266
|
+
"OPTIONS": {
|
|
267
|
+
"bucket_name": env.str("SECONDSHIFT_ARTIFACT_BUCKET", default=""),
|
|
268
|
+
"endpoint_url": env.str("SECONDSHIFT_ARTIFACT_ENDPOINT_URL", default=None),
|
|
269
|
+
"region_name": env.str("SECONDSHIFT_ARTIFACT_REGION", default=None),
|
|
270
|
+
# Unset keys fall back to boto3's credential chain (IAM roles etc.).
|
|
271
|
+
"access_key": env.str("SECONDSHIFT_ARTIFACT_ACCESS_KEY_ID", default=None),
|
|
272
|
+
"secret_key": env.str(
|
|
273
|
+
"SECONDSHIFT_ARTIFACT_SECRET_ACCESS_KEY", default=None
|
|
274
|
+
),
|
|
275
|
+
"location": env.str("SECONDSHIFT_ARTIFACT_PREFIX", default=""),
|
|
276
|
+
"default_acl": "private",
|
|
277
|
+
"querystring_auth": True,
|
|
278
|
+
"file_overwrite": True,
|
|
279
|
+
"addressing_style": env.str(
|
|
280
|
+
"SECONDSHIFT_ARTIFACT_ADDRESSING_STYLE", default=None
|
|
281
|
+
),
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
if ARTIFACT_STORAGE not in ARTIFACT_STORAGES:
|
|
286
|
+
raise ImproperlyConfigured(
|
|
287
|
+
f"SECONDSHIFT_ARTIFACT_STORAGE must be one of {sorted(ARTIFACT_STORAGES)}"
|
|
288
|
+
)
|
|
289
|
+
STORAGES = {
|
|
290
|
+
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
291
|
+
"run_artifacts": ARTIFACT_STORAGES[ARTIFACT_STORAGE],
|
|
292
|
+
"staticfiles": {"BACKEND": "secondshift.config.static_storage.StaticFilesStorage"},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# --- Accounts and login ------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
298
|
+
{
|
|
299
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
|
300
|
+
},
|
|
301
|
+
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
302
|
+
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
303
|
+
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
304
|
+
]
|
|
305
|
+
AUTHENTICATION_BACKENDS = [
|
|
306
|
+
"axes.backends.AxesStandaloneBackend",
|
|
307
|
+
"django.contrib.auth.backends.ModelBackend",
|
|
308
|
+
"allauth.account.auth_backends.AuthenticationBackend",
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
# Email goes out only through EMAIL_URL, e.g. smtp://user:pass@host:587.
|
|
312
|
+
EMAIL_URL = env.str(AppEnv.EMAIL_URL, default="")
|
|
313
|
+
DEFAULT_FROM_EMAIL = env.str(AppEnv.FROM_EMAIL, default="webmaster@localhost")
|
|
314
|
+
if EMAIL_URL:
|
|
315
|
+
globals().update(env.email_url(AppEnv.EMAIL_URL))
|
|
316
|
+
elif SINGLE_HOST:
|
|
317
|
+
# One person at the terminal reads them there.
|
|
318
|
+
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
|
319
|
+
else:
|
|
320
|
+
# Nowhere to send them. They are dropped so reset links never reach logs.
|
|
321
|
+
EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
|
|
322
|
+
# Whether an email reaches anyone. Password resets are offered only then.
|
|
323
|
+
EMAIL_DELIVERED = bool(EMAIL_URL) or SINGLE_HOST
|
|
324
|
+
# Ask people to confirm their address only when an email can reach them.
|
|
325
|
+
ACCOUNT_EMAIL_VERIFICATION = "optional" if EMAIL_URL or DEV else "none"
|
|
326
|
+
|
|
327
|
+
ACCOUNT_LOGIN_METHODS = {"email"}
|
|
328
|
+
ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
|
|
329
|
+
ACCOUNT_ADAPTER = "secondshift.core.adapters.AccountAdapter"
|
|
330
|
+
SOCIALACCOUNT_ADAPTER = "secondshift.core.adapters.SocialAccountAdapter"
|
|
331
|
+
# New accounts (email or GitHub) are refused unless enabled, open in dev.
|
|
332
|
+
SIGNUP_ENABLED = env.bool("SECONDSHIFT_SIGNUP_ENABLED", default=DEV)
|
|
333
|
+
LOGIN_REDIRECT_URL = "/"
|
|
334
|
+
LOGOUT_REDIRECT_URL = "/"
|
|
335
|
+
GITHUB_CLIENT_ID = env.str("GITHUB_CLIENT_ID", default="")
|
|
336
|
+
GITHUB_CLIENT_SECRET = env.str("GITHUB_CLIENT_SECRET", default="")
|
|
337
|
+
SOCIALACCOUNT_PROVIDERS = {"github": {"SCOPE": ["read:user", "user:email"]}}
|
|
338
|
+
# Without an app, allauth hides the GitHub button on the sign-in page.
|
|
339
|
+
if GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET:
|
|
340
|
+
SOCIALACCOUNT_PROVIDERS["github"]["APP"] = {
|
|
341
|
+
"client_id": GITHUB_CLIENT_ID,
|
|
342
|
+
"secret": GITHUB_CLIENT_SECRET,
|
|
343
|
+
"key": "",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Lock out an IP after repeated failed logins (admin and allauth alike).
|
|
347
|
+
AXES_FAILURE_LIMIT = env.int("AXES_FAILURE_LIMIT", default=5)
|
|
348
|
+
AXES_COOLOFF_TIME = timedelta(minutes=env.int("AXES_COOLOFF_MINUTES", default=30))
|
|
349
|
+
AXES_LOCKOUT_PARAMETERS = ["ip_address"]
|
|
350
|
+
AXES_RESET_ON_SUCCESS = True
|
|
351
|
+
AXES_VERBOSE = False
|
|
352
|
+
# Number of trusted reverse proxies that append to X-Forwarded-For (1 behind
|
|
353
|
+
# kamal-proxy). With 0 the header is ignored so clients cannot spoof their IP.
|
|
354
|
+
AXES_IPWARE_PROXY_COUNT = env.int("SECONDSHIFT_TRUSTED_PROXY_COUNT", default=0) or None
|
|
355
|
+
AXES_IPWARE_META_PRECEDENCE_ORDER = (
|
|
356
|
+
("HTTP_X_FORWARDED_FOR", "REMOTE_ADDR")
|
|
357
|
+
if AXES_IPWARE_PROXY_COUNT
|
|
358
|
+
else ("REMOTE_ADDR",)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# --- Browser security --------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
CSRF_TRUSTED_ORIGINS = env.list(
|
|
364
|
+
AppEnv.CSRF_TRUSTED_ORIGINS, default=[FRONTEND_URL.rstrip("/")]
|
|
365
|
+
)
|
|
366
|
+
X_FRAME_OPTIONS = "DENY"
|
|
367
|
+
# PrimeVue injects component styles at runtime, so styles need 'unsafe-inline'.
|
|
368
|
+
# Scripts stay restricted to same-origin files.
|
|
369
|
+
SECURE_CSP = {
|
|
370
|
+
"default-src": [CSP.SELF],
|
|
371
|
+
"script-src": [CSP.SELF],
|
|
372
|
+
"style-src": [CSP.SELF, CSP.UNSAFE_INLINE, "https://fonts.googleapis.com"],
|
|
373
|
+
"font-src": [CSP.SELF, "https://fonts.gstatic.com"],
|
|
374
|
+
"img-src": [CSP.SELF, "data:"],
|
|
375
|
+
"connect-src": [CSP.SELF],
|
|
376
|
+
"form-action": [CSP.SELF, GITHUB_BASE_URL],
|
|
377
|
+
"frame-ancestors": [CSP.NONE],
|
|
378
|
+
"base-uri": [CSP.SELF],
|
|
379
|
+
"object-src": [CSP.NONE],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# --- Repositories and runner -------------------------------------------------
|
|
383
|
+
|
|
384
|
+
# Reading repositories from this machine's filesystem is a development feature.
|
|
385
|
+
LOCAL_REPOSITORIES_ENABLED = SINGLE_HOST and env.bool(
|
|
386
|
+
"SECONDSHIFT_LOCAL_REPOSITORIES_ENABLED", default=True
|
|
387
|
+
)
|
|
388
|
+
# The control plane's copies of repository sources: one Git mirror per
|
|
389
|
+
# repository and the snapshots runs get. Both are caches. Deleting them only
|
|
390
|
+
# costs a fetch from the source again.
|
|
391
|
+
SOURCE_CACHE_ROOT = Path(
|
|
392
|
+
env.str(AppEnv.SOURCE_CACHE_ROOT, default=str(DATA_DIR / "sources"))
|
|
393
|
+
).resolve()
|
|
394
|
+
# Tasks and jobs may run skills from other public GitHub repositories.
|
|
395
|
+
EXTERNAL_SKILLS_ENABLED = env.bool(AppEnv.EXTERNAL_SKILLS_ENABLED, default=True)
|
|
396
|
+
# Where skills from other repositories are fetched from. Tests point it at a
|
|
397
|
+
# local folder. Only public repositories are read, without credentials.
|
|
398
|
+
EXTERNAL_SKILLS_BASE_URL = GITHUB_BASE_URL
|
|
399
|
+
# Snapshots past this size are removed, oldest first.
|
|
400
|
+
SOURCE_CACHE_MAX_BYTES = env.int(
|
|
401
|
+
AppEnv.SOURCE_CACHE_MAX_BYTES, default=2 * 1024 * 1024 * 1024
|
|
402
|
+
)
|
|
403
|
+
# Run limits. These are the defaults. A workspace owner can choose other values
|
|
404
|
+
# for the workspace or a job, up to the *_CEILING maximums. Repository manifests
|
|
405
|
+
# cannot set them. The values that apply are frozen into each execution plan.
|
|
406
|
+
# The ceilings are the highest limits a workspace owner can choose for a
|
|
407
|
+
# workspace or a job. The defaults suit short reviews. The ceilings leave room
|
|
408
|
+
# for long jobs, such as an audit, so an owner can raise one job's limits
|
|
409
|
+
# without a server change.
|
|
410
|
+
AGENT_MAX_TURNS, AGENT_MAX_TURNS_CEILING = run_limit(
|
|
411
|
+
"AGENT_MAX_TURNS", default=24, ceiling=200, minimum=1
|
|
412
|
+
)
|
|
413
|
+
AGENT_MAX_TOOL_CALLS, AGENT_MAX_TOOL_CALLS_CEILING = run_limit(
|
|
414
|
+
"AGENT_MAX_TOOL_CALLS", default=150, ceiling=500, minimum=0
|
|
415
|
+
)
|
|
416
|
+
AGENT_MAX_TOOL_RESULT_CHARS = env.int(
|
|
417
|
+
"SECONDSHIFT_AGENT_MAX_TOOL_RESULT_CHARS", default=65_536
|
|
418
|
+
)
|
|
419
|
+
if AGENT_MAX_TOOL_RESULT_CHARS < 1:
|
|
420
|
+
raise ImproperlyConfigured(
|
|
421
|
+
"SECONDSHIFT_AGENT_MAX_TOOL_RESULT_CHARS must be positive"
|
|
422
|
+
)
|
|
423
|
+
RUNNER_RUNTIME = env.str(AppEnv.RUNNER_RUNTIME, default=SandboxRuntime.DOCKER)
|
|
424
|
+
RUNNER_IMAGE = env.str(AppEnv.RUNNER_IMAGE, default=DEFAULT_SANDBOX_IMAGE)
|
|
425
|
+
# The orchestrator receives only this runner-scoped credential. It never receives
|
|
426
|
+
# the runtime socket or runner database credentials.
|
|
427
|
+
# On a single host the runner and gateway listen next to the web app.
|
|
428
|
+
RUNNER_SERVICE_URL = env.str(
|
|
429
|
+
"SECONDSHIFT_RUNNER_SERVICE_URL",
|
|
430
|
+
default="http://127.0.0.1:8002" if SINGLE_HOST else "",
|
|
431
|
+
)
|
|
432
|
+
RUNNER_SERVICE_TOKEN = env.str(AppEnv.RUNNER_SERVICE_TOKEN, default="")
|
|
433
|
+
# The trusted agent orchestrator calls this internal gateway. Command sandboxes
|
|
434
|
+
# always use --network=none and never receive its run-scoped token.
|
|
435
|
+
MODEL_GATEWAY_URL = env.str(
|
|
436
|
+
"SECONDSHIFT_MODEL_GATEWAY_URL",
|
|
437
|
+
default=LOCAL_GATEWAY_URL if SINGLE_HOST else "",
|
|
438
|
+
)
|
|
439
|
+
RUN_TIMEOUT_SECONDS, RUN_TIMEOUT_SECONDS_CEILING = run_limit(
|
|
440
|
+
"RUN_TIMEOUT_SECONDS", default=900, ceiling=7200, minimum=1
|
|
441
|
+
)
|
|
442
|
+
RUN_OUTPUT_MAX_BYTES = env.int(AppEnv.RUN_OUTPUT_MAX_BYTES, default=5 * 1024 * 1024)
|
|
443
|
+
RUN_OUTPUT_EXCERPT_BYTES = 16 * 1024
|
|
444
|
+
# The most earlier reports a job can ask for with `history` in its manifest.
|
|
445
|
+
JOB_HISTORY_MAX = env.int("SECONDSHIFT_JOB_HISTORY_MAX", default=5)
|
|
446
|
+
if JOB_HISTORY_MAX < 0:
|
|
447
|
+
raise ImproperlyConfigured("SECONDSHIFT_JOB_HISTORY_MAX cannot be negative")
|
|
448
|
+
# The most sub-agents one run may start, whatever its manifest asks for.
|
|
449
|
+
RUN_SUBAGENTS_MAX = env.int("SECONDSHIFT_RUN_SUBAGENTS_MAX", default=5)
|
|
450
|
+
if RUN_SUBAGENTS_MAX < 0:
|
|
451
|
+
raise ImproperlyConfigured("SECONDSHIFT_RUN_SUBAGENTS_MAX cannot be negative")
|
|
452
|
+
# The longest an API token can last. Its creator can choose a shorter time.
|
|
453
|
+
API_TOKEN_MAX_DAYS = env.int("SECONDSHIFT_API_TOKEN_MAX_DAYS", default=90)
|
|
454
|
+
if API_TOKEN_MAX_DAYS < 1:
|
|
455
|
+
raise ImproperlyConfigured("SECONDSHIFT_API_TOKEN_MAX_DAYS must be positive")
|
|
456
|
+
# Requests to /mcp each person can make per minute, across all their API tokens.
|
|
457
|
+
MCP_RATE_LIMIT = env.int("SECONDSHIFT_MCP_RATE_LIMIT", default=300)
|
|
458
|
+
if MCP_RATE_LIMIT < 1:
|
|
459
|
+
raise ImproperlyConfigured("SECONDSHIFT_MCP_RATE_LIMIT must be positive")
|
|
460
|
+
# Total text a run starts with from its job's earlier reports, newest first.
|
|
461
|
+
RUN_HISTORY_MAX_CHARS = env.int("SECONDSHIFT_RUN_HISTORY_MAX_CHARS", default=65_536)
|
|
462
|
+
if RUN_HISTORY_MAX_CHARS < 1:
|
|
463
|
+
raise ImproperlyConfigured("SECONDSHIFT_RUN_HISTORY_MAX_CHARS must be positive")
|
|
464
|
+
# Finished runs, their output and DBOS records are deleted after this. 0 keeps them.
|
|
465
|
+
RUN_RETENTION_DAYS = env.int(AppEnv.RUN_RETENTION_DAYS, default=90)
|
|
466
|
+
# How often the worker syncs local repositories whose branch has new commits.
|
|
467
|
+
# 0 turns automatic sync off.
|
|
468
|
+
REPOSITORY_SYNC_INTERVAL_SECONDS = env.int(
|
|
469
|
+
AppEnv.REPOSITORY_SYNC_INTERVAL_SECONDS, default=300
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# --- Outbound calls and model gateway ----------------------------------------
|
|
473
|
+
|
|
474
|
+
# Webhooks and model providers may only reach public HTTPS endpoints unless
|
|
475
|
+
# local targets are allowed (HTTP, localhost, private networks): dev only.
|
|
476
|
+
OUTBOUND_ALLOW_LOCAL = SINGLE_HOST and env.bool(
|
|
477
|
+
"SECONDSHIFT_OUTBOUND_ALLOW_LOCAL", default=True
|
|
478
|
+
)
|
|
479
|
+
OUTBOUND_TIMEOUT_SECONDS = 10
|
|
480
|
+
# Inbound job triggers: the largest request body accepted, how old a signed
|
|
481
|
+
# timestamp may be, and how many received requests each trigger keeps.
|
|
482
|
+
TRIGGER_MAX_BODY_BYTES = 1024 * 1024
|
|
483
|
+
TRIGGER_SIGNATURE_MAX_AGE_SECONDS = 300
|
|
484
|
+
TRIGGER_EVENTS_KEPT = 100
|
|
485
|
+
# A run keeps this much of the body that started it. The rest is cut.
|
|
486
|
+
TRIGGER_PAYLOAD_MAX_BYTES = 64 * 1024
|
|
487
|
+
# Runs one trigger may start per hour. An owner can change it per trigger, up
|
|
488
|
+
# to the ceiling.
|
|
489
|
+
TRIGGER_MAX_RUNS_PER_HOUR = env.int("SECONDSHIFT_TRIGGER_MAX_RUNS_PER_HOUR", default=10)
|
|
490
|
+
TRIGGER_MAX_RUNS_PER_HOUR_CEILING = env.int(
|
|
491
|
+
"SECONDSHIFT_TRIGGER_MAX_RUNS_PER_HOUR_CEILING", default=60
|
|
492
|
+
)
|
|
493
|
+
# Per-run token budget (input + output + cache tokens), largest accepted
|
|
494
|
+
# request body, and how long to wait between streamed chunks.
|
|
495
|
+
MODEL_RUN_TOKEN_BUDGET, MODEL_RUN_TOKEN_BUDGET_CEILING = run_limit(
|
|
496
|
+
"MODEL_RUN_TOKEN_BUDGET", default=2_000_000, ceiling=20_000_000, minimum=1
|
|
497
|
+
)
|
|
498
|
+
GATEWAY_MAX_BODY_BYTES = 32 * 1024 * 1024
|
|
499
|
+
GATEWAY_READ_TIMEOUT_SECONDS = 600
|
|
500
|
+
|
|
501
|
+
# GitHub App credentials belong only to the control plane, never to sandboxes.
|
|
502
|
+
GITHUB_APP_ID = env.str("SECONDSHIFT_GITHUB_APP_ID", default="")
|
|
503
|
+
GITHUB_APP_SLUG = env.str("SECONDSHIFT_GITHUB_APP_SLUG", default="")
|
|
504
|
+
GITHUB_APP_CLIENT_ID = env.str("SECONDSHIFT_GITHUB_APP_CLIENT_ID", default="")
|
|
505
|
+
GITHUB_APP_CLIENT_SECRET = env.str("SECONDSHIFT_GITHUB_APP_CLIENT_SECRET", default="")
|
|
506
|
+
GITHUB_APP_PRIVATE_KEY = env.str(
|
|
507
|
+
"SECONDSHIFT_GITHUB_APP_PRIVATE_KEY", default=""
|
|
508
|
+
).replace("\\n", "\n")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# CLI agent execution. Servers turn it on by setting an image pinned by digest.
|
|
512
|
+
# `secondshift up` builds the local tag, which its runner resolves per run.
|
|
513
|
+
AGENT_IMAGE = env.str(
|
|
514
|
+
AppEnv.AGENT_IMAGE,
|
|
515
|
+
default=LOCAL_AGENT_TAG if ENVIRONMENT == Environment.LOCAL else "",
|
|
516
|
+
)
|
|
517
|
+
AGENT_NETWORK = env.str(AppEnv.AGENT_NETWORK, default="bridge")
|
|
518
|
+
AGENT_BROKER_URL = env.str(
|
|
519
|
+
AppEnv.AGENT_BROKER_URL,
|
|
520
|
+
default=local_agent_broker_url(MODEL_GATEWAY_URL) if SINGLE_HOST else "",
|
|
521
|
+
)
|
|
522
|
+
# Agent containers call the gateway by the broker's host name.
|
|
523
|
+
if AGENT_IMAGE and (agent_broker_host := urlsplit(AGENT_BROKER_URL).hostname):
|
|
524
|
+
ALLOWED_HOSTS = list(dict.fromkeys([*ALLOWED_HOSTS, agent_broker_host]))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# ruff: noqa: F403, F405
|
|
2
|
+
from secondshift.config.settings.base import *
|
|
3
|
+
|
|
4
|
+
if not DEV:
|
|
5
|
+
raise ImproperlyConfigured(
|
|
6
|
+
"secondshift.config.settings.dev requires SECONDSHIFT_ENV=dev"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
INSTALLED_APPS += ["django_extensions"]
|
|
10
|
+
# The Vite dev server calls the API from another origin. Its proxy rewrites
|
|
11
|
+
# Host to the backend, so the browser Origin must also be trusted for CSRF.
|
|
12
|
+
CORS_ALLOWED_ORIGINS = [FRONTEND_URL]
|
|
13
|
+
CORS_ALLOW_CREDENTIALS = True
|
|
14
|
+
CSRF_TRUSTED_ORIGINS = [*CSRF_TRUSTED_ORIGINS, FRONTEND_URL]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ruff: noqa: F403, F405
|
|
2
|
+
"""`secondshift up`: one person running SecondShift on their own machine."""
|
|
3
|
+
|
|
4
|
+
from secondshift.config.constants import Environment
|
|
5
|
+
from secondshift.config.settings.base import *
|
|
6
|
+
|
|
7
|
+
if ENVIRONMENT != Environment.LOCAL:
|
|
8
|
+
raise ImproperlyConfigured(
|
|
9
|
+
"secondshift.config.settings.local requires SECONDSHIFT_ENV=local"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# `secondshift up` serves the dashboard here unless told otherwise. It sets
|
|
13
|
+
# FRONTEND_URL for its processes when --host or --port change it.
|
|
14
|
+
FRONTEND_URL = env.str(AppEnv.PUBLIC_URL, default="http://127.0.0.1:8000")
|
|
15
|
+
|
|
16
|
+
# Static files and the dashboard are served straight from the installed
|
|
17
|
+
# package, so there is no collectstatic step.
|
|
18
|
+
STATIC_ROOT = None
|
|
19
|
+
WHITENOISE_USE_FINDERS = True
|
|
20
|
+
STORAGES = {
|
|
21
|
+
**STORAGES,
|
|
22
|
+
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
|
|
23
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ruff: noqa: F403, F405
|
|
2
|
+
from secondshift.config.constants import (
|
|
3
|
+
AppEnv,
|
|
4
|
+
ArtifactStorage,
|
|
5
|
+
Environment,
|
|
6
|
+
SandboxRuntime,
|
|
7
|
+
Service,
|
|
8
|
+
)
|
|
9
|
+
from secondshift.config.settings.base import *
|
|
10
|
+
|
|
11
|
+
if ENVIRONMENT != Environment.PRODUCTION:
|
|
12
|
+
raise ImproperlyConfigured(
|
|
13
|
+
"secondshift.config.settings.production requires SECONDSHIFT_ENV=production"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Every problem is reported at once, and the process refuses to start.
|
|
17
|
+
errors: list[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def required(name: str) -> str:
|
|
21
|
+
value = env.str(name, default="")
|
|
22
|
+
if not value:
|
|
23
|
+
errors.append(f"{name} must be set")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# No development fallbacks for secrets or the database in production.
|
|
28
|
+
SECRET_KEY = required(AppEnv.SECRET_KEY)
|
|
29
|
+
FIELD_ENCRYPTION_KEY = required(AppEnv.ENCRYPTION_KEY)
|
|
30
|
+
required(AppEnv.DATABASE_URL)
|
|
31
|
+
if DEBUG:
|
|
32
|
+
errors.append(f"{AppEnv.DEBUG} must be off")
|
|
33
|
+
if CACHES["default"]["BACKEND"] in {
|
|
34
|
+
"django.core.cache.backends.locmem.LocMemCache",
|
|
35
|
+
"django.core.cache.backends.dummy.DummyCache",
|
|
36
|
+
}:
|
|
37
|
+
errors.append(f"{AppEnv.CACHE_URL} must be shared by every process")
|
|
38
|
+
if SERVICE == Service.WEB and not CSRF_TRUSTED_ORIGINS:
|
|
39
|
+
errors.append(f"{AppEnv.CSRF_TRUSTED_ORIGINS} must be set")
|
|
40
|
+
if SERVICE in {Service.WEB, Service.WORKER}:
|
|
41
|
+
try:
|
|
42
|
+
public_origin = urlsplit(FRONTEND_URL)
|
|
43
|
+
valid_origin = (
|
|
44
|
+
public_origin.scheme == "https"
|
|
45
|
+
and bool(public_origin.hostname)
|
|
46
|
+
and not public_origin.username
|
|
47
|
+
and not public_origin.password
|
|
48
|
+
and public_origin.path in {"", "/"}
|
|
49
|
+
and not public_origin.query
|
|
50
|
+
and not public_origin.fragment
|
|
51
|
+
and public_origin.port != 0
|
|
52
|
+
)
|
|
53
|
+
except ValueError:
|
|
54
|
+
valid_origin = False
|
|
55
|
+
if not valid_origin:
|
|
56
|
+
errors.append(f"{AppEnv.PUBLIC_URL} must be the public HTTPS address")
|
|
57
|
+
if (
|
|
58
|
+
ARTIFACT_STORAGE == ArtifactStorage.S3
|
|
59
|
+
and not ARTIFACT_STORAGES[ArtifactStorage.S3]["OPTIONS"]["bucket_name"]
|
|
60
|
+
):
|
|
61
|
+
errors.append("SECONDSHIFT_ARTIFACT_BUCKET must be set for s3 storage")
|
|
62
|
+
if RUNNER_RUNTIME != SandboxRuntime.GVISOR:
|
|
63
|
+
errors.append("SECONDSHIFT_RUNNER_RUNTIME must be runsc (gVisor)")
|
|
64
|
+
if TASK_QUEUE_LIMIT < 1:
|
|
65
|
+
errors.append(f"{AppEnv.TASK_QUEUE_LIMIT} must be positive")
|
|
66
|
+
if SERVICE == Service.WORKER:
|
|
67
|
+
if DBOS_WORKER_CONCURRENCY < 1:
|
|
68
|
+
errors.append(f"{AppEnv.RUN_CONCURRENCY} must be positive")
|
|
69
|
+
if DBOS_GLOBAL_CONCURRENCY < 0:
|
|
70
|
+
errors.append(f"{AppEnv.RUN_GLOBAL_CONCURRENCY} must be zero or positive")
|
|
71
|
+
required(AppEnv.RUNNER_SERVICE_URL)
|
|
72
|
+
required(AppEnv.MODEL_GATEWAY_URL)
|
|
73
|
+
if len(required(AppEnv.RUNNER_SERVICE_TOKEN)) < 32:
|
|
74
|
+
errors.append(
|
|
75
|
+
"SECONDSHIFT_RUNNER_SERVICE_TOKEN must contain at least 32 characters"
|
|
76
|
+
)
|
|
77
|
+
# Partial App configuration must not silently disable production repository sync.
|
|
78
|
+
app_configuration = (
|
|
79
|
+
GITHUB_APP_ID,
|
|
80
|
+
GITHUB_APP_SLUG,
|
|
81
|
+
GITHUB_APP_CLIENT_ID,
|
|
82
|
+
GITHUB_APP_CLIENT_SECRET,
|
|
83
|
+
GITHUB_APP_PRIVATE_KEY,
|
|
84
|
+
)
|
|
85
|
+
if SERVICE in {Service.WEB, Service.WORKER} and any(app_configuration):
|
|
86
|
+
if not all(app_configuration):
|
|
87
|
+
errors.append(
|
|
88
|
+
"All SECONDSHIFT_GITHUB_APP_* settings must be configured together"
|
|
89
|
+
)
|
|
90
|
+
if SERVICE == Service.WEB:
|
|
91
|
+
required(AppEnv.RUNNER_SERVICE_URL)
|
|
92
|
+
if len(required(AppEnv.RUNNER_SERVICE_TOKEN)) < 32:
|
|
93
|
+
errors.append(
|
|
94
|
+
"SECONDSHIFT_RUNNER_SERVICE_TOKEN must contain at least 32 characters"
|
|
95
|
+
)
|
|
96
|
+
if errors:
|
|
97
|
+
raise ImproperlyConfigured(
|
|
98
|
+
"Production settings are incomplete:\n- " + "\n- ".join(errors)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# --- HTTPS hardening ---------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
# Trust X-Forwarded-Proto only from a TLS-terminating proxy that overwrites it
|
|
104
|
+
# (kamal-proxy does). The app container must not be reachable any other way.
|
|
105
|
+
if env.bool("SECONDSHIFT_BEHIND_TLS_PROXY", default=True):
|
|
106
|
+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
107
|
+
# The gateway process is reached only by trusted orchestration services.
|
|
108
|
+
SECURE_SSL_REDIRECT = SERVICE != Service.GATEWAY
|
|
109
|
+
SESSION_COOKIE_SECURE = True
|
|
110
|
+
CSRF_COOKIE_SECURE = True
|
|
111
|
+
SECURE_HSTS_SECONDS = 31536000
|
|
112
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
113
|
+
SECURE_HSTS_PRELOAD = True
|