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,100 @@
|
|
|
1
|
+
from secondshift.connections.execution import recent_login_runs
|
|
2
|
+
from secondshift.connections.github import is_github_source, shows_attribution
|
|
3
|
+
from secondshift.connections.mcp.base import SOURCE_AUTH
|
|
4
|
+
from secondshift.connections.mcp.oauth import requested_scopes
|
|
5
|
+
from secondshift.connections.models import AgentConnection, Connection
|
|
6
|
+
from secondshift.connections.schemas import (
|
|
7
|
+
ConnectionSchema,
|
|
8
|
+
ConnectionUsageSchema,
|
|
9
|
+
OfferedToolSchema,
|
|
10
|
+
)
|
|
11
|
+
from secondshift.connections.services import (
|
|
12
|
+
connection_target,
|
|
13
|
+
)
|
|
14
|
+
from secondshift.runs.reporting.connection_usage import ConnectionUsage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def connection_settings_view(connection: Connection) -> dict[str, str]:
|
|
18
|
+
"""Owner-chosen settings that are safe to show. Never a credential."""
|
|
19
|
+
if connection.kind == Connection.Kind.MCP:
|
|
20
|
+
return connection.settings
|
|
21
|
+
if connection.kind == Connection.Kind.API:
|
|
22
|
+
return {
|
|
23
|
+
**connection.settings,
|
|
24
|
+
"base_url": connection.endpoint_url,
|
|
25
|
+
"spec_url": connection.metadata.get("spec_url", ""),
|
|
26
|
+
}
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def connection_auth(connection: Connection) -> str:
|
|
31
|
+
"""How the connection signs in: oauth, token, app, or empty."""
|
|
32
|
+
if connection.kind == Connection.Kind.SOURCE:
|
|
33
|
+
return (
|
|
34
|
+
"app"
|
|
35
|
+
if connection.auth_strategy == Connection.AuthStrategy.MANAGED
|
|
36
|
+
else "token"
|
|
37
|
+
)
|
|
38
|
+
if connection.kind not in {Connection.Kind.MCP, Connection.Kind.API}:
|
|
39
|
+
return ""
|
|
40
|
+
if connection.auth_strategy == Connection.AuthStrategy.OAUTH:
|
|
41
|
+
return "oauth"
|
|
42
|
+
if connection.auth_strategy == Connection.AuthStrategy.MANAGED:
|
|
43
|
+
return SOURCE_AUTH
|
|
44
|
+
return "token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def serialize_connection(
|
|
48
|
+
connection: Connection, usage: ConnectionUsage | None = None
|
|
49
|
+
) -> ConnectionSchema:
|
|
50
|
+
return ConnectionSchema(
|
|
51
|
+
id=connection.id,
|
|
52
|
+
provider=connection.provider,
|
|
53
|
+
kind=connection.kind,
|
|
54
|
+
binding_key=connection.binding_key,
|
|
55
|
+
display_name=connection.display_name,
|
|
56
|
+
status=connection.current_status,
|
|
57
|
+
expires_at=connection.expires_at,
|
|
58
|
+
available_tools=connection.available_tools,
|
|
59
|
+
target=connection_target(connection),
|
|
60
|
+
allowed_models=connection.allowed_models,
|
|
61
|
+
default_model=connection.default_model,
|
|
62
|
+
is_default=connection.kind == Connection.Kind.MODEL
|
|
63
|
+
and connection.workspace.default_model_connection_id == connection.pk,
|
|
64
|
+
available_models=list(connection.metadata.get("available_models", [])),
|
|
65
|
+
last_check=connection.last_check,
|
|
66
|
+
settings=connection_settings_view(connection),
|
|
67
|
+
auth=connection_auth(connection),
|
|
68
|
+
scopes=list(connection.scopes)
|
|
69
|
+
if connection.kind == Connection.Kind.MCP
|
|
70
|
+
else [],
|
|
71
|
+
requested_scopes=requested_scopes(connection)
|
|
72
|
+
if connection.kind == Connection.Kind.MCP
|
|
73
|
+
and connection.auth_strategy == Connection.AuthStrategy.OAUTH
|
|
74
|
+
else [],
|
|
75
|
+
offered_tools=[
|
|
76
|
+
OfferedToolSchema(
|
|
77
|
+
name=name,
|
|
78
|
+
description=str(tool.get("description", ""))[:500],
|
|
79
|
+
read_only=bool(tool.get("readOnly")),
|
|
80
|
+
)
|
|
81
|
+
for name, tool in sorted(connection.offered_tools.items())
|
|
82
|
+
],
|
|
83
|
+
require_approval=connection.require_approval,
|
|
84
|
+
attribution=is_github_source(connection) and shows_attribution(connection),
|
|
85
|
+
usage=None
|
|
86
|
+
if usage is None
|
|
87
|
+
else ConnectionUsageSchema(
|
|
88
|
+
**{**vars(usage), "spend_7d_usd": float(usage.spend_7d_usd)}
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def with_recent_runs(
|
|
94
|
+
connections: list[AgentConnection], workspace_id: int
|
|
95
|
+
) -> list[AgentConnection]:
|
|
96
|
+
"""Attach each login's recent run count for AgentConnectionSchema."""
|
|
97
|
+
counts = recent_login_runs(workspace_id)
|
|
98
|
+
for connection in connections:
|
|
99
|
+
connection.recent_runs = counts[connection.pk]
|
|
100
|
+
return connections
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from django.db import IntegrityError, transaction
|
|
2
|
+
|
|
3
|
+
from secondshift.connections.dao import binding_taken
|
|
4
|
+
from secondshift.connections.models import Connection
|
|
5
|
+
from secondshift.connections.outbound import host_label
|
|
6
|
+
from secondshift.connections.webhooks import validate_webhook_url
|
|
7
|
+
from secondshift.workspaces.models import Workspace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionConflict(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def connection_target(connection: Connection) -> str:
|
|
15
|
+
"""The host a connection reaches, as saved when it was set up."""
|
|
16
|
+
return str(connection.metadata.get("target", ""))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_webhook_connection(
|
|
20
|
+
workspace: Workspace,
|
|
21
|
+
binding_key: str,
|
|
22
|
+
display_name: str,
|
|
23
|
+
url: str,
|
|
24
|
+
) -> Connection:
|
|
25
|
+
url = validate_webhook_url(url)
|
|
26
|
+
if binding_taken(workspace.pk, binding_key):
|
|
27
|
+
raise ConnectionConflict(
|
|
28
|
+
f"A connection for {binding_key} already exists in this workspace."
|
|
29
|
+
)
|
|
30
|
+
connection = Connection(
|
|
31
|
+
workspace=workspace,
|
|
32
|
+
provider=Connection.Provider.WEBHOOK,
|
|
33
|
+
kind=Connection.Kind.NOTIFICATION,
|
|
34
|
+
binding_key=binding_key,
|
|
35
|
+
display_name=display_name.strip() or binding_key,
|
|
36
|
+
auth_strategy=Connection.AuthStrategy.NONE,
|
|
37
|
+
delivery_url=url,
|
|
38
|
+
metadata={"target": host_label(url)},
|
|
39
|
+
)
|
|
40
|
+
connection.full_clean()
|
|
41
|
+
try:
|
|
42
|
+
with transaction.atomic():
|
|
43
|
+
connection.save()
|
|
44
|
+
except IntegrityError as error:
|
|
45
|
+
raise ConnectionConflict(
|
|
46
|
+
f"A connection for {binding_key} already exists in this workspace."
|
|
47
|
+
) from error
|
|
48
|
+
return connection
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def update_webhook_connection(
|
|
52
|
+
connection: Connection,
|
|
53
|
+
display_name: str | None,
|
|
54
|
+
url: str | None,
|
|
55
|
+
) -> Connection:
|
|
56
|
+
if connection.provider != Connection.Provider.WEBHOOK:
|
|
57
|
+
raise ConnectionConflict("Only webhook connections can be edited here.")
|
|
58
|
+
if display_name is not None:
|
|
59
|
+
connection.display_name = display_name.strip() or connection.binding_key
|
|
60
|
+
if url:
|
|
61
|
+
url = validate_webhook_url(url)
|
|
62
|
+
connection.delivery_url = url
|
|
63
|
+
connection.metadata = {
|
|
64
|
+
**connection.metadata,
|
|
65
|
+
"target": host_label(url),
|
|
66
|
+
}
|
|
67
|
+
connection.status = Connection.Status.CONNECTED
|
|
68
|
+
elif connection.status == Connection.Status.REVOKED:
|
|
69
|
+
raise ConnectionConflict("Enter a new URL to reconnect this webhook.")
|
|
70
|
+
connection.full_clean()
|
|
71
|
+
connection.save()
|
|
72
|
+
return connection
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def revoke_connection(connection: Connection) -> Connection:
|
|
76
|
+
"""Stop all use immediately and discard stored credentials; keep the record."""
|
|
77
|
+
connection.status = Connection.Status.REVOKED
|
|
78
|
+
connection.delivery_url = ""
|
|
79
|
+
connection.access_token = ""
|
|
80
|
+
connection.refresh_token = ""
|
|
81
|
+
connection.metadata = {
|
|
82
|
+
key: value for key, value in connection.metadata.items() if key != "target"
|
|
83
|
+
}
|
|
84
|
+
connection.save(
|
|
85
|
+
update_fields=[
|
|
86
|
+
"status",
|
|
87
|
+
"delivery_url",
|
|
88
|
+
"access_token",
|
|
89
|
+
"refresh_token",
|
|
90
|
+
"metadata",
|
|
91
|
+
"updated_at",
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
return connection
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""What a connection's server offers, and which of those tools jobs may use."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from secondshift.connections.models import Connection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnknownTools(ValueError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def store_tools(
|
|
13
|
+
connection: Connection,
|
|
14
|
+
offered: dict[str, Any],
|
|
15
|
+
selected: list[str] | None,
|
|
16
|
+
*,
|
|
17
|
+
owner_selects: bool = True,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Save the offered tools and the ones jobs may use.
|
|
20
|
+
|
|
21
|
+
When the owner chooses, tools marked read-only start chosen. Otherwise
|
|
22
|
+
jobs may use every offered tool. Raises UnknownTools for a name the server
|
|
23
|
+
does not offer.
|
|
24
|
+
"""
|
|
25
|
+
if not owner_selects:
|
|
26
|
+
chosen = set(offered)
|
|
27
|
+
elif selected is None:
|
|
28
|
+
chosen = {name for name, tool in offered.items() if tool.get("readOnly")}
|
|
29
|
+
elif set(selected) - set(offered):
|
|
30
|
+
raise UnknownTools
|
|
31
|
+
else:
|
|
32
|
+
chosen = set(selected)
|
|
33
|
+
connection.available_tools = sorted(chosen)
|
|
34
|
+
connection.metadata = {
|
|
35
|
+
**connection.metadata,
|
|
36
|
+
"tools": {name: offered[name] for name in sorted(chosen)},
|
|
37
|
+
"offered_tools": offered if owner_selects else {},
|
|
38
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
from secondshift.connections.checks import record_check
|
|
9
|
+
from secondshift.connections.models import Connection
|
|
10
|
+
from secondshift.connections.outbound import (
|
|
11
|
+
OutboundUrlError,
|
|
12
|
+
PinnedSyncTransport,
|
|
13
|
+
validate_outbound_url,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
USER_AGENT = "SecondShift-Webhook/1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
WebhookError = OutboundUrlError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class WebhookDelivery:
|
|
26
|
+
ok: bool
|
|
27
|
+
status_code: int | None
|
|
28
|
+
message: str
|
|
29
|
+
retryable: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_webhook_url(url: str) -> str:
|
|
33
|
+
return validate_outbound_url(url, "webhook URL")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def webhook_transport() -> httpx.BaseTransport:
|
|
37
|
+
# Connects only to the addresses it checked, so DNS cannot rebind
|
|
38
|
+
# between the check and the request.
|
|
39
|
+
return PinnedSyncTransport()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def deliver_webhook(connection: Connection, payload: dict[str, Any]) -> WebhookDelivery:
|
|
43
|
+
"""Send the payload and keep the result, so the dashboard shows the last delivery."""
|
|
44
|
+
delivery = post_webhook(connection, payload)
|
|
45
|
+
if connection.pk:
|
|
46
|
+
record_check(connection, delivery.ok, delivery.message)
|
|
47
|
+
return delivery
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def post_webhook(connection: Connection, payload: dict[str, Any]) -> WebhookDelivery:
|
|
51
|
+
if (
|
|
52
|
+
connection.provider != Connection.Provider.WEBHOOK
|
|
53
|
+
or not connection.delivery_url
|
|
54
|
+
):
|
|
55
|
+
return WebhookDelivery(False, None, "This connection has no webhook URL.")
|
|
56
|
+
try:
|
|
57
|
+
# Validated again at send time: DNS answers can change after saving.
|
|
58
|
+
url = validate_webhook_url(connection.delivery_url)
|
|
59
|
+
except WebhookError as error:
|
|
60
|
+
return WebhookDelivery(False, None, str(error))
|
|
61
|
+
try:
|
|
62
|
+
with httpx.Client(
|
|
63
|
+
transport=webhook_transport(),
|
|
64
|
+
timeout=settings.OUTBOUND_TIMEOUT_SECONDS,
|
|
65
|
+
follow_redirects=False,
|
|
66
|
+
) as client:
|
|
67
|
+
response = client.post(
|
|
68
|
+
url, json=payload, headers={"User-Agent": USER_AGENT}
|
|
69
|
+
)
|
|
70
|
+
except httpx.TimeoutException:
|
|
71
|
+
return WebhookDelivery(False, None, "The webhook timed out.", retryable=True)
|
|
72
|
+
except httpx.HTTPError as error:
|
|
73
|
+
# Exception text includes the URL, which may embed a token.
|
|
74
|
+
logger.warning(
|
|
75
|
+
"webhook delivery to connection %s failed: %s",
|
|
76
|
+
connection.pk,
|
|
77
|
+
type(error).__name__,
|
|
78
|
+
)
|
|
79
|
+
return WebhookDelivery(
|
|
80
|
+
False, None, "The webhook could not be reached.", retryable=True
|
|
81
|
+
)
|
|
82
|
+
status = response.status_code
|
|
83
|
+
if 200 <= status < 300:
|
|
84
|
+
return WebhookDelivery(True, status, f"Delivered (HTTP {status}).")
|
|
85
|
+
if 300 <= status < 400:
|
|
86
|
+
return WebhookDelivery(
|
|
87
|
+
False,
|
|
88
|
+
status,
|
|
89
|
+
f"The webhook redirected (HTTP {status}); redirects are not followed.",
|
|
90
|
+
)
|
|
91
|
+
return WebhookDelivery(
|
|
92
|
+
False,
|
|
93
|
+
status,
|
|
94
|
+
f"The webhook answered HTTP {status}.",
|
|
95
|
+
retryable=status == 429 or status >= 500,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def connection_test_payload(connection: Connection) -> dict[str, Any]:
|
|
100
|
+
return {
|
|
101
|
+
"event": "connection.test",
|
|
102
|
+
"text": (
|
|
103
|
+
f"SecondShift test message for {connection.binding_key}. "
|
|
104
|
+
"Run reports for jobs using this binding will arrive here."
|
|
105
|
+
),
|
|
106
|
+
"binding": connection.binding_key,
|
|
107
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from allauth.account.adapter import DefaultAccountAdapter
|
|
4
|
+
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
|
5
|
+
from allauth.socialaccount.models import SocialLogin
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.http import HttpRequest
|
|
8
|
+
|
|
9
|
+
from secondshift.workspaces.invites import pending_invite
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def signup_open(request: HttpRequest) -> bool:
|
|
13
|
+
"""Open to everyone when enabled, else only to someone with an invite."""
|
|
14
|
+
return settings.SIGNUP_ENABLED or pending_invite(request) is not None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AccountAdapter(DefaultAccountAdapter):
|
|
18
|
+
def is_open_for_signup(self, request: HttpRequest) -> bool:
|
|
19
|
+
return signup_open(request)
|
|
20
|
+
|
|
21
|
+
def add_message(
|
|
22
|
+
self,
|
|
23
|
+
request: HttpRequest,
|
|
24
|
+
level: int,
|
|
25
|
+
message_template: str | None = None,
|
|
26
|
+
message_context: dict[str, Any] | None = None,
|
|
27
|
+
extra_tags: str = "",
|
|
28
|
+
message: str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Drop the message: most flows end in the dashboard, which never
|
|
31
|
+
shows them, so they piled up and appeared stale on a later page."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SocialAccountAdapter(DefaultSocialAccountAdapter):
|
|
35
|
+
def is_open_for_signup(
|
|
36
|
+
self, request: HttpRequest, sociallogin: SocialLogin
|
|
37
|
+
) -> bool:
|
|
38
|
+
return signup_open(request)
|
secondshift/core/api.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.http import HttpRequest
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
from ninja import Router
|
|
5
|
+
from ninja.security import django_auth
|
|
6
|
+
|
|
7
|
+
from secondshift.connections.dao import connected_count
|
|
8
|
+
from secondshift.connections.execution import execution_status
|
|
9
|
+
from secondshift.core.schemas import (
|
|
10
|
+
FailingJobSchema,
|
|
11
|
+
JobSpendSchema,
|
|
12
|
+
OverviewSchema,
|
|
13
|
+
RunDaySchema,
|
|
14
|
+
UserSchema,
|
|
15
|
+
)
|
|
16
|
+
from secondshift.github.app import configured as github_app_configured
|
|
17
|
+
from secondshift.jobs.dao import enabled_job_count
|
|
18
|
+
from secondshift.repositories.dao import active_repositories
|
|
19
|
+
from secondshift.repositories.models import Repository
|
|
20
|
+
from secondshift.runs.dao import run_counts_today
|
|
21
|
+
from secondshift.runs.reporting.health import (
|
|
22
|
+
failing_jobs,
|
|
23
|
+
runs_by_day,
|
|
24
|
+
spend_by_job,
|
|
25
|
+
week_summary,
|
|
26
|
+
)
|
|
27
|
+
from secondshift.workspaces.access import (
|
|
28
|
+
current_membership,
|
|
29
|
+
current_workspace,
|
|
30
|
+
is_operator,
|
|
31
|
+
permitted_actions,
|
|
32
|
+
)
|
|
33
|
+
from secondshift.workspaces.dao import has_own_token
|
|
34
|
+
from secondshift.workspaces.server_settings import (
|
|
35
|
+
environment_label,
|
|
36
|
+
sandbox_label,
|
|
37
|
+
)
|
|
38
|
+
from secondshift.workspaces.services import can_create_workspace
|
|
39
|
+
|
|
40
|
+
router = Router(auth=django_auth, tags=["core"])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/me", response=UserSchema, auth=django_auth)
|
|
44
|
+
def me(request: HttpRequest) -> UserSchema:
|
|
45
|
+
membership = current_membership(request)
|
|
46
|
+
workspace = membership.workspace if membership else None
|
|
47
|
+
return UserSchema(
|
|
48
|
+
email=request.user.email or request.user.get_username(),
|
|
49
|
+
workspace_name=workspace.name if workspace else None,
|
|
50
|
+
workspace_slug=workspace.slug if workspace else None,
|
|
51
|
+
role=membership.role if membership else None,
|
|
52
|
+
permissions=permitted_actions(request),
|
|
53
|
+
can_use_admin=is_operator(request),
|
|
54
|
+
can_create_workspace=can_create_workspace(request.user),
|
|
55
|
+
local_repositories_enabled=settings.LOCAL_REPOSITORIES_ENABLED,
|
|
56
|
+
github_app_configured=github_app_configured(),
|
|
57
|
+
environment=environment_label(),
|
|
58
|
+
sandbox=sandbox_label(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("/overview", response=OverviewSchema, auth=django_auth)
|
|
63
|
+
def overview(request: HttpRequest) -> OverviewSchema:
|
|
64
|
+
workspace = current_workspace(request)
|
|
65
|
+
if not workspace:
|
|
66
|
+
return OverviewSchema(
|
|
67
|
+
repositories=0,
|
|
68
|
+
local_repositories=0,
|
|
69
|
+
enabled_jobs=0,
|
|
70
|
+
runs_today=0,
|
|
71
|
+
failed_runs=0,
|
|
72
|
+
connected_services=0,
|
|
73
|
+
)
|
|
74
|
+
today = timezone.localdate()
|
|
75
|
+
repositories = active_repositories(workspace.pk)
|
|
76
|
+
counts = run_counts_today(workspace.pk, today)
|
|
77
|
+
week = week_summary(workspace)
|
|
78
|
+
return OverviewSchema(
|
|
79
|
+
repositories=repositories.count(),
|
|
80
|
+
local_repositories=repositories.filter(
|
|
81
|
+
source_type=Repository.SourceType.LOCAL
|
|
82
|
+
).count(),
|
|
83
|
+
enabled_jobs=enabled_job_count(workspace.pk),
|
|
84
|
+
runs_today=counts["today"],
|
|
85
|
+
failed_runs=counts["failed"],
|
|
86
|
+
connected_services=connected_count(workspace.pk),
|
|
87
|
+
failing_jobs=[
|
|
88
|
+
FailingJobSchema(**vars(failing)) for failing in failing_jobs(workspace)
|
|
89
|
+
],
|
|
90
|
+
runs_7d=week.runs,
|
|
91
|
+
runs_previous_7d=week.previous_runs,
|
|
92
|
+
finished_7d=week.finished,
|
|
93
|
+
succeeded_7d=week.succeeded,
|
|
94
|
+
spend_7d_usd=float(week.spend_usd),
|
|
95
|
+
spend_previous_7d_usd=float(week.previous_spend_usd),
|
|
96
|
+
spend_partial=week.spend_partial,
|
|
97
|
+
spend_by_job=[
|
|
98
|
+
JobSpendSchema(**{**vars(row), "spend_usd": float(row.spend_usd)})
|
|
99
|
+
for row in spend_by_job(workspace)
|
|
100
|
+
],
|
|
101
|
+
run_days=[RunDaySchema(**vars(day)) for day in runs_by_day(workspace)],
|
|
102
|
+
execution=execution_status(workspace),
|
|
103
|
+
has_agent_token=has_own_token(request.user, workspace),
|
|
104
|
+
)
|
secondshift/core/apps.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
from django.core.management import call_command
|
|
5
|
+
from django.db.models.signals import post_migrate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_cache_tables(using: str, **kwargs: Any) -> None:
|
|
9
|
+
"""Create the database cache table, so every migrate leaves it ready."""
|
|
10
|
+
call_command("createcachetable", database=using, verbosity=0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CoreConfig(AppConfig):
|
|
14
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
15
|
+
name = "secondshift.core"
|
|
16
|
+
|
|
17
|
+
def ready(self) -> None:
|
|
18
|
+
# Django sends post_migrate only for apps with models, and core has
|
|
19
|
+
# none. Listen to every app. The command skips a table that exists.
|
|
20
|
+
post_migrate.connect(create_cache_tables, dispatch_uid="create_cache_tables")
|