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,169 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RunLimitOverrides(models.Model):
|
|
6
|
+
"""Run limits an owner chose. Empty means the next level up decides.
|
|
7
|
+
|
|
8
|
+
A job falls back to its workspace, and a workspace to the server defaults.
|
|
9
|
+
The server maximums cap every value (see secondshift.workspaces.limits).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
max_turns = models.PositiveIntegerField(null=True, blank=True)
|
|
13
|
+
max_tool_calls = models.PositiveIntegerField(null=True, blank=True)
|
|
14
|
+
token_budget = models.PositiveBigIntegerField(null=True, blank=True)
|
|
15
|
+
timeout_seconds = models.PositiveIntegerField(null=True, blank=True)
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
abstract = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Workspace(RunLimitOverrides):
|
|
22
|
+
"""Who belongs and with which role is kept in WorkspaceMembership only."""
|
|
23
|
+
|
|
24
|
+
# What runs the jobs: one model key or one subscription login, never both.
|
|
25
|
+
# Neither set means nothing is chosen and runs cannot start.
|
|
26
|
+
default_model_connection = models.ForeignKey(
|
|
27
|
+
"connections.Connection",
|
|
28
|
+
on_delete=models.SET_NULL,
|
|
29
|
+
null=True,
|
|
30
|
+
blank=True,
|
|
31
|
+
limit_choices_to={"kind": "model"},
|
|
32
|
+
related_name="default_workspaces",
|
|
33
|
+
)
|
|
34
|
+
default_agent_connection = models.ForeignKey(
|
|
35
|
+
"connections.AgentConnection",
|
|
36
|
+
on_delete=models.RESTRICT,
|
|
37
|
+
null=True,
|
|
38
|
+
blank=True,
|
|
39
|
+
related_name="default_workspaces",
|
|
40
|
+
)
|
|
41
|
+
name = models.CharField(max_length=160)
|
|
42
|
+
slug = models.SlugField(unique=True)
|
|
43
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
44
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
45
|
+
|
|
46
|
+
class Meta(RunLimitOverrides.Meta):
|
|
47
|
+
ordering = ["name"]
|
|
48
|
+
constraints = [
|
|
49
|
+
models.CheckConstraint(
|
|
50
|
+
condition=models.Q(default_model_connection__isnull=True)
|
|
51
|
+
| models.Q(default_agent_connection__isnull=True),
|
|
52
|
+
name="workspace_one_execution_default",
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def __str__(self) -> str:
|
|
57
|
+
return self.name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class WorkspaceMembership(models.Model):
|
|
61
|
+
class Role(models.TextChoices):
|
|
62
|
+
OWNER = "owner", "Owner"
|
|
63
|
+
ADMIN = "admin", "Admin"
|
|
64
|
+
MEMBER = "member", "Member"
|
|
65
|
+
|
|
66
|
+
workspace = models.ForeignKey(
|
|
67
|
+
Workspace,
|
|
68
|
+
on_delete=models.CASCADE,
|
|
69
|
+
related_name="memberships",
|
|
70
|
+
)
|
|
71
|
+
user = models.ForeignKey(
|
|
72
|
+
settings.AUTH_USER_MODEL,
|
|
73
|
+
on_delete=models.CASCADE,
|
|
74
|
+
related_name="workspace_memberships",
|
|
75
|
+
)
|
|
76
|
+
role = models.CharField(max_length=16, choices=Role.choices, default=Role.MEMBER)
|
|
77
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
78
|
+
|
|
79
|
+
class Meta:
|
|
80
|
+
constraints = [
|
|
81
|
+
models.UniqueConstraint(
|
|
82
|
+
fields=["workspace", "user"],
|
|
83
|
+
name="unique_workspace_membership",
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
return f"{self.user} in {self.workspace}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class WorkspaceInvite(models.Model):
|
|
92
|
+
"""A one-time link that lets someone join a workspace with a role.
|
|
93
|
+
|
|
94
|
+
Only a hash of the link's token is stored, so the database alone cannot
|
|
95
|
+
be used to join. See secondshift.workspaces.invites.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
workspace = models.ForeignKey(
|
|
99
|
+
Workspace,
|
|
100
|
+
on_delete=models.CASCADE,
|
|
101
|
+
related_name="invites",
|
|
102
|
+
)
|
|
103
|
+
role = models.CharField(
|
|
104
|
+
max_length=16,
|
|
105
|
+
choices=WorkspaceMembership.Role.choices,
|
|
106
|
+
default=WorkspaceMembership.Role.MEMBER,
|
|
107
|
+
)
|
|
108
|
+
# Who the link is for, so owners can tell pending invites apart.
|
|
109
|
+
note = models.CharField(max_length=160, blank=True)
|
|
110
|
+
token_hash = models.CharField(max_length=64, unique=True)
|
|
111
|
+
created_by = models.ForeignKey(
|
|
112
|
+
settings.AUTH_USER_MODEL,
|
|
113
|
+
on_delete=models.SET_NULL,
|
|
114
|
+
null=True,
|
|
115
|
+
blank=True,
|
|
116
|
+
related_name="+",
|
|
117
|
+
)
|
|
118
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
119
|
+
expires_at = models.DateTimeField()
|
|
120
|
+
accepted_by = models.ForeignKey(
|
|
121
|
+
settings.AUTH_USER_MODEL,
|
|
122
|
+
on_delete=models.SET_NULL,
|
|
123
|
+
null=True,
|
|
124
|
+
blank=True,
|
|
125
|
+
related_name="+",
|
|
126
|
+
)
|
|
127
|
+
accepted_at = models.DateTimeField(null=True, blank=True)
|
|
128
|
+
revoked_at = models.DateTimeField(null=True, blank=True)
|
|
129
|
+
|
|
130
|
+
class Meta:
|
|
131
|
+
ordering = ["-created_at", "-pk"]
|
|
132
|
+
|
|
133
|
+
def __str__(self) -> str:
|
|
134
|
+
return f"{self.get_role_display()} invite to {self.workspace}"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ApiToken(models.Model):
|
|
138
|
+
"""A bearer token for the MCP server, for one person in one workspace.
|
|
139
|
+
|
|
140
|
+
Only a hash of the token is stored. See secondshift.workspaces.tokens.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
class Scope(models.TextChoices):
|
|
144
|
+
READ = "read", "Read"
|
|
145
|
+
RUN = "run", "Read and run"
|
|
146
|
+
|
|
147
|
+
workspace = models.ForeignKey(
|
|
148
|
+
Workspace,
|
|
149
|
+
on_delete=models.CASCADE,
|
|
150
|
+
related_name="api_tokens",
|
|
151
|
+
)
|
|
152
|
+
created_by = models.ForeignKey(
|
|
153
|
+
settings.AUTH_USER_MODEL,
|
|
154
|
+
on_delete=models.CASCADE,
|
|
155
|
+
related_name="api_tokens",
|
|
156
|
+
)
|
|
157
|
+
name = models.CharField(max_length=160)
|
|
158
|
+
scope = models.CharField(max_length=16, choices=Scope.choices, default=Scope.READ)
|
|
159
|
+
token_hash = models.CharField(max_length=64, unique=True)
|
|
160
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
161
|
+
expires_at = models.DateTimeField()
|
|
162
|
+
last_used_at = models.DateTimeField(null=True, blank=True)
|
|
163
|
+
revoked_at = models.DateTimeField(null=True, blank=True)
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
ordering = ["-created_at", "-pk"]
|
|
167
|
+
|
|
168
|
+
def __str__(self) -> str:
|
|
169
|
+
return f"{self.name} ({self.get_scope_display()})"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The first account on a single-host install and the workspace it owns."""
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth.models import AbstractUser
|
|
4
|
+
|
|
5
|
+
from secondshift.workspaces.dao import (
|
|
6
|
+
create_workspace,
|
|
7
|
+
ensure_membership,
|
|
8
|
+
find_workspace_by_slug,
|
|
9
|
+
get_or_create_user,
|
|
10
|
+
get_workspace_by_slug,
|
|
11
|
+
workspace_owner,
|
|
12
|
+
workspace_slug_has_owner,
|
|
13
|
+
)
|
|
14
|
+
from secondshift.workspaces.models import Workspace, WorkspaceMembership
|
|
15
|
+
|
|
16
|
+
WORKSPACE_SLUG = "secondshift"
|
|
17
|
+
WORKSPACE_NAME = "SecondShift"
|
|
18
|
+
# The owner's username when `secondshift up` creates it without asking.
|
|
19
|
+
LOCAL_OWNER_USERNAME = "owner"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def has_owner() -> bool:
|
|
23
|
+
return workspace_slug_has_owner(WORKSPACE_SLUG)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_owner() -> AbstractUser:
|
|
27
|
+
return workspace_owner(get_workspace_by_slug(WORKSPACE_SLUG)) # type: ignore[return-value]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_owner(email: str = "", password: str | None = None) -> Workspace:
|
|
31
|
+
"""Create the owner and the workspace when they do not exist yet.
|
|
32
|
+
|
|
33
|
+
Without a password the owner can only sign in with a link from
|
|
34
|
+
`secondshift up` or `secondshift login`.
|
|
35
|
+
"""
|
|
36
|
+
user, created = get_or_create_user(
|
|
37
|
+
email or LOCAL_OWNER_USERNAME,
|
|
38
|
+
{"email": email, "is_staff": True, "is_superuser": True},
|
|
39
|
+
)
|
|
40
|
+
if created:
|
|
41
|
+
if password:
|
|
42
|
+
user.set_password(password)
|
|
43
|
+
else:
|
|
44
|
+
user.set_unusable_password()
|
|
45
|
+
user.save()
|
|
46
|
+
workspace = find_workspace_by_slug(WORKSPACE_SLUG)
|
|
47
|
+
if workspace is None:
|
|
48
|
+
return create_workspace(WORKSPACE_NAME, WORKSPACE_SLUG, user)
|
|
49
|
+
ensure_membership(workspace, user, WorkspaceMembership.Role.OWNER)
|
|
50
|
+
return workspace
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_owner_credentials(email: str, password: str) -> None:
|
|
54
|
+
"""Let the owner sign in with an email and password too."""
|
|
55
|
+
user = get_owner()
|
|
56
|
+
user.email = email
|
|
57
|
+
user.set_password(password)
|
|
58
|
+
user.save()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from ninja import Schema
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LimitSchema(Schema):
|
|
9
|
+
key: str
|
|
10
|
+
label: str
|
|
11
|
+
help: str
|
|
12
|
+
unit: str
|
|
13
|
+
value: int
|
|
14
|
+
override: int | None
|
|
15
|
+
inherited: int
|
|
16
|
+
minimum: int
|
|
17
|
+
maximum: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunLimitsUpdateSchema(Schema):
|
|
21
|
+
"""Fields left out stay as they are. Null uses the inherited value."""
|
|
22
|
+
|
|
23
|
+
max_turns: int | None = None
|
|
24
|
+
max_tool_calls: int | None = None
|
|
25
|
+
token_budget: int | None = None
|
|
26
|
+
timeout_seconds: int | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SettingValueSchema(Schema):
|
|
30
|
+
label: str
|
|
31
|
+
value: str
|
|
32
|
+
env: str
|
|
33
|
+
help: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SettingGroupSchema(Schema):
|
|
37
|
+
title: str
|
|
38
|
+
items: list[SettingValueSchema]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WorkspaceSettingsSchema(Schema):
|
|
42
|
+
workspace_name: str | None
|
|
43
|
+
workspace_slug: str | None
|
|
44
|
+
environment: str
|
|
45
|
+
sandbox: str
|
|
46
|
+
groups: list[SettingGroupSchema]
|
|
47
|
+
# Run limits for the whole workspace. Each job can choose its own.
|
|
48
|
+
limits: list[LimitSchema]
|
|
49
|
+
can_edit_limits: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WorkspaceChoiceSchema(Schema):
|
|
53
|
+
id: int
|
|
54
|
+
name: str
|
|
55
|
+
slug: str
|
|
56
|
+
role: str
|
|
57
|
+
current: bool
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class WorkspaceSelectSchema(Schema):
|
|
61
|
+
workspace_id: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WorkspaceCreateSchema(Schema):
|
|
65
|
+
name: str = Field(max_length=160)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MemberSchema(Schema):
|
|
69
|
+
id: int
|
|
70
|
+
email: str
|
|
71
|
+
role: str
|
|
72
|
+
joined_at: datetime
|
|
73
|
+
you: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
RoleName = Literal["owner", "admin", "member"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RoleChangeSchema(Schema):
|
|
80
|
+
role: RoleName
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InviteCreateSchema(Schema):
|
|
84
|
+
role: RoleName = "member"
|
|
85
|
+
note: str = Field("", max_length=160)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class InviteSchema(Schema):
|
|
89
|
+
id: int
|
|
90
|
+
role: str
|
|
91
|
+
note: str
|
|
92
|
+
created_by: str | None
|
|
93
|
+
created_at: datetime
|
|
94
|
+
expires_at: datetime
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NewInviteSchema(InviteSchema):
|
|
98
|
+
# Shown once, right after creating it. It cannot be looked up later.
|
|
99
|
+
link: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PendingInviteSchema(Schema):
|
|
103
|
+
workspace_name: str
|
|
104
|
+
role: str
|
|
105
|
+
invited_by: str | None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
ScopeName = Literal["read", "run"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ApiTokenSchema(Schema):
|
|
112
|
+
id: int
|
|
113
|
+
name: str
|
|
114
|
+
scope: str
|
|
115
|
+
workspace_id: int
|
|
116
|
+
workspace_name: str
|
|
117
|
+
workspace_slug: str
|
|
118
|
+
created_at: datetime
|
|
119
|
+
expires_at: datetime
|
|
120
|
+
last_used_at: datetime | None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TokenWorkspaceSchema(Schema):
|
|
124
|
+
"""A workspace the person can create a token for."""
|
|
125
|
+
|
|
126
|
+
id: int
|
|
127
|
+
name: str
|
|
128
|
+
slug: str
|
|
129
|
+
current: bool
|
|
130
|
+
can_create_run_tokens: bool
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ApiTokenListSchema(Schema):
|
|
134
|
+
items: list[ApiTokenSchema]
|
|
135
|
+
workspaces: list[TokenWorkspaceSchema]
|
|
136
|
+
# The longest expiry, in days, a new token can have.
|
|
137
|
+
max_days: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ApiTokenCreateSchema(Schema):
|
|
141
|
+
workspace_id: int
|
|
142
|
+
name: str = Field(min_length=1, max_length=160)
|
|
143
|
+
scope: ScopeName = "read"
|
|
144
|
+
expires_in_days: int
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class NewApiTokenSchema(ApiTokenSchema):
|
|
148
|
+
# Shown once, right after creating it. It cannot be looked up later.
|
|
149
|
+
token: str
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from django.http import HttpRequest
|
|
2
|
+
|
|
3
|
+
from secondshift.workspaces.access import (
|
|
4
|
+
Action,
|
|
5
|
+
allows,
|
|
6
|
+
is_operator,
|
|
7
|
+
)
|
|
8
|
+
from secondshift.workspaces.limits import LimitError, LimitView, workspace_limit_views
|
|
9
|
+
from secondshift.workspaces.models import (
|
|
10
|
+
ApiToken,
|
|
11
|
+
Workspace,
|
|
12
|
+
WorkspaceInvite,
|
|
13
|
+
WorkspaceMembership,
|
|
14
|
+
)
|
|
15
|
+
from secondshift.workspaces.schemas import (
|
|
16
|
+
ApiTokenSchema,
|
|
17
|
+
InviteSchema,
|
|
18
|
+
LimitSchema,
|
|
19
|
+
MemberSchema,
|
|
20
|
+
SettingGroupSchema,
|
|
21
|
+
SettingValueSchema,
|
|
22
|
+
TokenWorkspaceSchema,
|
|
23
|
+
WorkspaceChoiceSchema,
|
|
24
|
+
WorkspaceSettingsSchema,
|
|
25
|
+
)
|
|
26
|
+
from secondshift.workspaces.server_settings import (
|
|
27
|
+
SettingValue,
|
|
28
|
+
environment_label,
|
|
29
|
+
sandbox_label,
|
|
30
|
+
setting_groups,
|
|
31
|
+
)
|
|
32
|
+
from secondshift.workspaces.tokens import scope_allowed
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def serialize_setting(request: HttpRequest, item: SettingValue) -> SettingValueSchema:
|
|
36
|
+
# Only people who run the server can set the variable, so only they see it.
|
|
37
|
+
env = item.env if is_operator(request) else ""
|
|
38
|
+
return SettingValueSchema(**{**vars(item), "env": env})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def serialize_settings(
|
|
42
|
+
request: HttpRequest, workspace: Workspace | None
|
|
43
|
+
) -> WorkspaceSettingsSchema:
|
|
44
|
+
return WorkspaceSettingsSchema(
|
|
45
|
+
workspace_name=workspace.name if workspace else None,
|
|
46
|
+
workspace_slug=workspace.slug if workspace else None,
|
|
47
|
+
environment=environment_label(),
|
|
48
|
+
sandbox=sandbox_label(),
|
|
49
|
+
groups=[
|
|
50
|
+
SettingGroupSchema(
|
|
51
|
+
title=group.title,
|
|
52
|
+
items=[serialize_setting(request, item) for item in group.items],
|
|
53
|
+
)
|
|
54
|
+
for group in setting_groups()
|
|
55
|
+
],
|
|
56
|
+
limits=serialize_limits(workspace_limit_views(workspace)) if workspace else [],
|
|
57
|
+
can_edit_limits=allows(request, Action.CHANGE_RUN_LIMITS),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def serialize_choice(
|
|
62
|
+
membership: WorkspaceMembership, current_id: int | None
|
|
63
|
+
) -> WorkspaceChoiceSchema:
|
|
64
|
+
workspace = membership.workspace
|
|
65
|
+
return WorkspaceChoiceSchema(
|
|
66
|
+
id=workspace.pk,
|
|
67
|
+
name=workspace.name,
|
|
68
|
+
slug=workspace.slug,
|
|
69
|
+
role=membership.role,
|
|
70
|
+
current=workspace.pk == current_id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def person_label(user: object | None) -> str | None:
|
|
75
|
+
if user is None:
|
|
76
|
+
return None
|
|
77
|
+
return getattr(user, "email", "") or user.get_username()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def serialize_invite(invite: WorkspaceInvite) -> InviteSchema:
|
|
81
|
+
return InviteSchema(
|
|
82
|
+
id=invite.pk,
|
|
83
|
+
role=invite.role,
|
|
84
|
+
note=invite.note,
|
|
85
|
+
created_by=person_label(invite.created_by),
|
|
86
|
+
created_at=invite.created_at,
|
|
87
|
+
expires_at=invite.expires_at,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def serialize_member(
|
|
92
|
+
request: HttpRequest, membership: WorkspaceMembership
|
|
93
|
+
) -> MemberSchema:
|
|
94
|
+
return MemberSchema(
|
|
95
|
+
id=membership.pk,
|
|
96
|
+
email=person_label(membership.user) or "",
|
|
97
|
+
role=membership.role,
|
|
98
|
+
joined_at=membership.created_at,
|
|
99
|
+
you=membership.user_id == request.user.pk,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def serialize_api_token(token: ApiToken) -> ApiTokenSchema:
|
|
104
|
+
return ApiTokenSchema(
|
|
105
|
+
id=token.pk,
|
|
106
|
+
name=token.name,
|
|
107
|
+
scope=token.scope,
|
|
108
|
+
workspace_id=token.workspace_id,
|
|
109
|
+
workspace_name=token.workspace.name,
|
|
110
|
+
workspace_slug=token.workspace.slug,
|
|
111
|
+
created_at=token.created_at,
|
|
112
|
+
expires_at=token.expires_at,
|
|
113
|
+
last_used_at=token.last_used_at,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def serialize_token_workspace(
|
|
118
|
+
membership: WorkspaceMembership, current_id: int | None
|
|
119
|
+
) -> TokenWorkspaceSchema:
|
|
120
|
+
workspace = membership.workspace
|
|
121
|
+
return TokenWorkspaceSchema(
|
|
122
|
+
id=workspace.pk,
|
|
123
|
+
name=workspace.name,
|
|
124
|
+
slug=workspace.slug,
|
|
125
|
+
current=workspace.pk == current_id,
|
|
126
|
+
can_create_run_tokens=scope_allowed(membership.role, ApiToken.Scope.RUN),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def serialize_limits(views: list[LimitView]) -> list[LimitSchema]:
|
|
131
|
+
return [LimitSchema(**vars(view)) for view in views]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def limit_error_detail(request: HttpRequest, error: LimitError) -> str:
|
|
135
|
+
"""Only people who run the server can raise a maximum, so only they get the hint."""
|
|
136
|
+
if error.ceiling_env and is_operator(request):
|
|
137
|
+
return f"{error} Set {error.ceiling_env} to allow more."
|
|
138
|
+
return str(error)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""The settings page: what the server enforces.
|
|
2
|
+
|
|
3
|
+
Runner settings are server-owned (see AGENTS.md), so they are shown with the
|
|
4
|
+
environment variable that changes them rather than edited here. Run limits are
|
|
5
|
+
edited on the page within server maximums (see secondshift.workspaces.limits).
|
|
6
|
+
Nothing secret is included: no keys, connection URLs or filesystem paths.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
|
|
13
|
+
from secondshift.config.constants import (
|
|
14
|
+
DEFAULT_SANDBOX_IMAGE,
|
|
15
|
+
AppEnv,
|
|
16
|
+
ArtifactStorage,
|
|
17
|
+
Environment,
|
|
18
|
+
SandboxRuntime,
|
|
19
|
+
)
|
|
20
|
+
from secondshift.config.formatting import duration_label
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SettingValue:
|
|
25
|
+
label: str
|
|
26
|
+
value: str
|
|
27
|
+
env: str
|
|
28
|
+
help: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class SettingGroup:
|
|
33
|
+
title: str
|
|
34
|
+
items: list[SettingValue]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def size_label(value: int) -> str:
|
|
38
|
+
for unit, factor in (("MB", 1024 * 1024), ("KB", 1024)):
|
|
39
|
+
if value >= factor and value % factor == 0:
|
|
40
|
+
return f"{value // factor} {unit}"
|
|
41
|
+
return f"{value:,} bytes"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def environment_label() -> str:
|
|
45
|
+
return {Environment.DEV: "Development", Environment.LOCAL: "This computer"}.get(
|
|
46
|
+
settings.ENVIRONMENT, "Production"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sandbox_label() -> str:
|
|
51
|
+
"""How command sandboxes are isolated, in words."""
|
|
52
|
+
if settings.RUNNER_RUNTIME == SandboxRuntime.GVISOR:
|
|
53
|
+
return "gVisor sandbox"
|
|
54
|
+
return "Trusted Docker"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def image_label(image: str) -> str:
|
|
58
|
+
"""The image name with a short digest, e.g. python:3.13-slim @ 57b29a7c050c."""
|
|
59
|
+
name, _, digest = image.partition("@sha256:")
|
|
60
|
+
return f"{name} @ {digest[:12]}" if digest else image
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def repository_groups() -> list[SettingGroup]:
|
|
64
|
+
"""Background sync applies only to local repositories."""
|
|
65
|
+
if not settings.LOCAL_REPOSITORIES_ENABLED:
|
|
66
|
+
return []
|
|
67
|
+
interval = settings.REPOSITORY_SYNC_INTERVAL_SECONDS
|
|
68
|
+
return [
|
|
69
|
+
SettingGroup(
|
|
70
|
+
"Repositories",
|
|
71
|
+
[
|
|
72
|
+
SettingValue(
|
|
73
|
+
"Background sync",
|
|
74
|
+
f"Every {duration_label(interval)}" if interval > 0 else "Off",
|
|
75
|
+
AppEnv.REPOSITORY_SYNC_INTERVAL_SECONDS,
|
|
76
|
+
"New commits on a repository's branch apply on their own. "
|
|
77
|
+
"Each repository can turn this off.",
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def setting_groups() -> list[SettingGroup]:
|
|
85
|
+
retention = settings.RUN_RETENTION_DAYS
|
|
86
|
+
return [
|
|
87
|
+
SettingGroup(
|
|
88
|
+
"Runs",
|
|
89
|
+
[
|
|
90
|
+
SettingValue(
|
|
91
|
+
"History kept for",
|
|
92
|
+
f"{retention} days" if retention > 0 else "Forever",
|
|
93
|
+
AppEnv.RUN_RETENTION_DAYS,
|
|
94
|
+
"Finished runs and their files are deleted after this.",
|
|
95
|
+
),
|
|
96
|
+
SettingValue(
|
|
97
|
+
"Largest stored output",
|
|
98
|
+
size_label(settings.RUN_OUTPUT_MAX_BYTES),
|
|
99
|
+
AppEnv.RUN_OUTPUT_MAX_BYTES,
|
|
100
|
+
"Longer reports keep their start and end.",
|
|
101
|
+
),
|
|
102
|
+
SettingValue(
|
|
103
|
+
"File storage",
|
|
104
|
+
"Object storage (S3)"
|
|
105
|
+
if settings.ARTIFACT_STORAGE == ArtifactStorage.S3
|
|
106
|
+
else "Server filesystem",
|
|
107
|
+
AppEnv.ARTIFACT_STORAGE,
|
|
108
|
+
"Where run input and output files are kept.",
|
|
109
|
+
),
|
|
110
|
+
],
|
|
111
|
+
),
|
|
112
|
+
*repository_groups(),
|
|
113
|
+
SettingGroup(
|
|
114
|
+
"Sandbox",
|
|
115
|
+
[
|
|
116
|
+
SettingValue(
|
|
117
|
+
"Isolation",
|
|
118
|
+
sandbox_label(),
|
|
119
|
+
AppEnv.RUNNER_RUNTIME,
|
|
120
|
+
"Production requires gVisor and refuses to start without it.",
|
|
121
|
+
),
|
|
122
|
+
SettingValue(
|
|
123
|
+
"Network",
|
|
124
|
+
"None",
|
|
125
|
+
"",
|
|
126
|
+
"Skills cannot reach the network. Model and approved MCP "
|
|
127
|
+
"calls go through SecondShift.",
|
|
128
|
+
),
|
|
129
|
+
SettingValue(
|
|
130
|
+
"Image",
|
|
131
|
+
image_label(settings.RUNNER_IMAGE),
|
|
132
|
+
AppEnv.RUNNER_IMAGE,
|
|
133
|
+
"Repositories cannot choose the image."
|
|
134
|
+
if settings.RUNNER_IMAGE != DEFAULT_SANDBOX_IMAGE
|
|
135
|
+
else "Docker's official Python image, pinned. "
|
|
136
|
+
"Repositories cannot choose the image.",
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
),
|
|
140
|
+
]
|