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,747 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.exceptions import ValidationError
|
|
6
|
+
from django.http import HttpRequest, HttpResponseRedirect
|
|
7
|
+
from django.shortcuts import get_object_or_404
|
|
8
|
+
from ninja import Router, Status
|
|
9
|
+
from ninja.errors import HttpError
|
|
10
|
+
from ninja.security import django_auth
|
|
11
|
+
|
|
12
|
+
from secondshift.config.constants import SUMMARY_MAX_CHARS
|
|
13
|
+
from secondshift.connections.agents import (
|
|
14
|
+
AgentConnectionError,
|
|
15
|
+
check_agent_connection,
|
|
16
|
+
disconnect_agent_connection,
|
|
17
|
+
reconnect_agent_connection,
|
|
18
|
+
save_agent_connection,
|
|
19
|
+
)
|
|
20
|
+
from secondshift.connections.dao import (
|
|
21
|
+
github_token_sources,
|
|
22
|
+
own_agent_connections,
|
|
23
|
+
workspace_agent_connections,
|
|
24
|
+
workspace_connections,
|
|
25
|
+
)
|
|
26
|
+
from secondshift.connections.execution import (
|
|
27
|
+
apply_model_default,
|
|
28
|
+
execution_configuration,
|
|
29
|
+
save_execution_preference,
|
|
30
|
+
use_when_nothing_runs,
|
|
31
|
+
)
|
|
32
|
+
from secondshift.connections.github import (
|
|
33
|
+
connect_token_source,
|
|
34
|
+
is_github_source,
|
|
35
|
+
set_attribution,
|
|
36
|
+
)
|
|
37
|
+
from secondshift.connections.http_api.connections import (
|
|
38
|
+
create_api_connection,
|
|
39
|
+
recheck_api_connection,
|
|
40
|
+
summarize,
|
|
41
|
+
update_api_connection,
|
|
42
|
+
)
|
|
43
|
+
from secondshift.connections.http_api.spec import APIError
|
|
44
|
+
from secondshift.connections.mcp.base import MCPError
|
|
45
|
+
from secondshift.connections.mcp.client import (
|
|
46
|
+
create_mcp_connection,
|
|
47
|
+
recheck_mcp_connection,
|
|
48
|
+
update_mcp_connection,
|
|
49
|
+
)
|
|
50
|
+
from secondshift.connections.mcp.oauth import inspect_server, requested_scopes
|
|
51
|
+
from secondshift.connections.mcp.providers import MCP_PROVIDERS
|
|
52
|
+
from secondshift.connections.mcp.signin import (
|
|
53
|
+
SignInError,
|
|
54
|
+
complete_authorization,
|
|
55
|
+
start_authorization,
|
|
56
|
+
)
|
|
57
|
+
from secondshift.connections.model_providers import (
|
|
58
|
+
CheckFailed,
|
|
59
|
+
ModelProviderError,
|
|
60
|
+
check_model_connection,
|
|
61
|
+
checked_api_key,
|
|
62
|
+
create_model_connection,
|
|
63
|
+
fetch_models,
|
|
64
|
+
update_model_connection,
|
|
65
|
+
validated_endpoint,
|
|
66
|
+
)
|
|
67
|
+
from secondshift.connections.models import AgentConnection, Connection
|
|
68
|
+
from secondshift.connections.schemas import (
|
|
69
|
+
AgentConnectionInput,
|
|
70
|
+
AgentConnectionSchema,
|
|
71
|
+
AgentRunLimitInput,
|
|
72
|
+
APIConnectionCreateSchema,
|
|
73
|
+
APIConnectionUpdateSchema,
|
|
74
|
+
APISpecSchema,
|
|
75
|
+
APISpecSummarySchema,
|
|
76
|
+
AttributionSchema,
|
|
77
|
+
ConnectionSchema,
|
|
78
|
+
ConnectionTestSchema,
|
|
79
|
+
ExecutionConfigurationSchema,
|
|
80
|
+
ExecutionPreferenceSchema,
|
|
81
|
+
GitHubTokenConnectSchema,
|
|
82
|
+
MCPConnectionCreateSchema,
|
|
83
|
+
MCPConnectionUpdateSchema,
|
|
84
|
+
MCPInspectResultSchema,
|
|
85
|
+
MCPInspectSchema,
|
|
86
|
+
MCPProviderSchema,
|
|
87
|
+
MCPSettingSchema,
|
|
88
|
+
MCPSignInSchema,
|
|
89
|
+
MCPSignInStartSchema,
|
|
90
|
+
ModelConnectionCreateSchema,
|
|
91
|
+
ModelConnectionUpdateSchema,
|
|
92
|
+
WebhookConnectionCreateSchema,
|
|
93
|
+
WebhookConnectionUpdateSchema,
|
|
94
|
+
WebhookSampleSchema,
|
|
95
|
+
)
|
|
96
|
+
from secondshift.connections.serialization import (
|
|
97
|
+
serialize_connection,
|
|
98
|
+
with_recent_runs,
|
|
99
|
+
)
|
|
100
|
+
from secondshift.connections.services import (
|
|
101
|
+
ConnectionConflict,
|
|
102
|
+
create_webhook_connection,
|
|
103
|
+
revoke_connection,
|
|
104
|
+
update_webhook_connection,
|
|
105
|
+
)
|
|
106
|
+
from secondshift.connections.webhooks import (
|
|
107
|
+
WebhookError,
|
|
108
|
+
connection_test_payload,
|
|
109
|
+
deliver_webhook,
|
|
110
|
+
)
|
|
111
|
+
from secondshift.github.errors import GitHubError
|
|
112
|
+
from secondshift.jobs.automatic_grants import approve_waiting_for, set_require_approval
|
|
113
|
+
from secondshift.jobs.grants import delete_connection
|
|
114
|
+
from secondshift.repositories.github_check import check_source
|
|
115
|
+
from secondshift.runs.reporting.connection_usage import (
|
|
116
|
+
ConnectionUsage,
|
|
117
|
+
connection_usage,
|
|
118
|
+
)
|
|
119
|
+
from secondshift.runs.reporting.notifications import sample_run_report_payload
|
|
120
|
+
from secondshift.workspaces.access import (
|
|
121
|
+
Action,
|
|
122
|
+
current_workspace,
|
|
123
|
+
require,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
router = Router(auth=django_auth, tags=["connections"])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.get("/connections", response=list[ConnectionSchema], auth=django_auth)
|
|
130
|
+
def connections(request: HttpRequest) -> list[ConnectionSchema]:
|
|
131
|
+
workspace = current_workspace(request)
|
|
132
|
+
if not workspace:
|
|
133
|
+
return []
|
|
134
|
+
usage = connection_usage(workspace)
|
|
135
|
+
return [
|
|
136
|
+
serialize_connection(item, usage.get(item.pk, ConnectionUsage()))
|
|
137
|
+
for item in workspace_connections(workspace.pk)
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@router.get(
|
|
142
|
+
"/connections/mcp-providers", response=list[MCPProviderSchema], auth=django_auth
|
|
143
|
+
)
|
|
144
|
+
def list_mcp_providers(request: HttpRequest) -> list[MCPProviderSchema]:
|
|
145
|
+
"""The built-in MCP servers an owner can connect. Never includes secrets."""
|
|
146
|
+
return [
|
|
147
|
+
MCPProviderSchema(
|
|
148
|
+
key=provider.key,
|
|
149
|
+
label=provider.label,
|
|
150
|
+
description=provider.description,
|
|
151
|
+
default_binding=provider.default_binding,
|
|
152
|
+
token_label=provider.token_label,
|
|
153
|
+
token_hint=provider.token_hint,
|
|
154
|
+
setup_url=provider.setup_url,
|
|
155
|
+
setup_help=provider.setup_help,
|
|
156
|
+
settings=[
|
|
157
|
+
MCPSettingSchema(
|
|
158
|
+
key=item.key,
|
|
159
|
+
label=item.label,
|
|
160
|
+
pattern=item.pattern,
|
|
161
|
+
required=item.required,
|
|
162
|
+
help=item.help,
|
|
163
|
+
placeholder=item.placeholder,
|
|
164
|
+
)
|
|
165
|
+
for item in provider.settings
|
|
166
|
+
],
|
|
167
|
+
tools=list(provider.tools),
|
|
168
|
+
auth_methods=list(provider.auth_methods),
|
|
169
|
+
owner_selects_tools=provider.owner_selects_tools,
|
|
170
|
+
)
|
|
171
|
+
for provider in MCP_PROVIDERS.values()
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@router.post("/connections/github-token", response=ConnectionSchema, auth=django_auth)
|
|
176
|
+
def connect_github_token(
|
|
177
|
+
request: HttpRequest, payload: GitHubTokenConnectSchema
|
|
178
|
+
) -> ConnectionSchema:
|
|
179
|
+
"""Reach repositories with a personal access token instead of the App."""
|
|
180
|
+
workspace = require(request, Action.ADD_GITHUB_REPOSITORIES)
|
|
181
|
+
replacing = None
|
|
182
|
+
if payload.connection_id is not None:
|
|
183
|
+
replacing = get_object_or_404(
|
|
184
|
+
github_token_sources(workspace.pk), pk=payload.connection_id
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
connection = connect_token_source(workspace, payload.token, replacing)
|
|
188
|
+
except GitHubError as error:
|
|
189
|
+
raise HttpError(422, str(error)) from error
|
|
190
|
+
return serialize_connection(connection)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.post(
|
|
194
|
+
"/connections/{connection_id}/attribution",
|
|
195
|
+
response=ConnectionSchema,
|
|
196
|
+
auth=django_auth,
|
|
197
|
+
)
|
|
198
|
+
def update_attribution(
|
|
199
|
+
request: HttpRequest, connection_id: int, payload: AttributionSchema
|
|
200
|
+
) -> ConnectionSchema:
|
|
201
|
+
"""Whether pull requests, issues and commits through this source name SecondShift."""
|
|
202
|
+
connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
|
|
203
|
+
if not is_github_source(connection):
|
|
204
|
+
raise HttpError(404, "Not Found")
|
|
205
|
+
return serialize_connection(set_attribution(connection, payload.attribution))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.post("/connections/mcp", response=ConnectionSchema, auth=django_auth)
|
|
209
|
+
def create_mcp(
|
|
210
|
+
request: HttpRequest, payload: MCPConnectionCreateSchema
|
|
211
|
+
) -> ConnectionSchema:
|
|
212
|
+
workspace = require(request, Action.ADD_MCP_CONNECTIONS)
|
|
213
|
+
try:
|
|
214
|
+
connection = create_mcp_connection(
|
|
215
|
+
workspace,
|
|
216
|
+
payload.provider,
|
|
217
|
+
payload.binding_key,
|
|
218
|
+
payload.display_name,
|
|
219
|
+
payload.token,
|
|
220
|
+
payload.settings,
|
|
221
|
+
payload.auth,
|
|
222
|
+
)
|
|
223
|
+
except ConnectionConflict as error:
|
|
224
|
+
raise HttpError(409, str(error)) from error
|
|
225
|
+
except MCPError as error:
|
|
226
|
+
raise HttpError(422, str(error)) from error
|
|
227
|
+
except ValidationError as error:
|
|
228
|
+
raise HttpError(422, "The MCP connection configuration is invalid.") from error
|
|
229
|
+
return apply_approval(connection, payload.require_approval)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@router.post(
|
|
233
|
+
"/connections/{connection_id}/mcp",
|
|
234
|
+
response=ConnectionSchema,
|
|
235
|
+
auth=django_auth,
|
|
236
|
+
)
|
|
237
|
+
def update_mcp(
|
|
238
|
+
request: HttpRequest, connection_id: int, payload: MCPConnectionUpdateSchema
|
|
239
|
+
) -> ConnectionSchema:
|
|
240
|
+
connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
|
|
241
|
+
try:
|
|
242
|
+
connection = update_mcp_connection(
|
|
243
|
+
connection,
|
|
244
|
+
payload.display_name,
|
|
245
|
+
payload.token,
|
|
246
|
+
payload.settings,
|
|
247
|
+
payload.tools,
|
|
248
|
+
)
|
|
249
|
+
except ConnectionConflict as error:
|
|
250
|
+
raise HttpError(409, str(error)) from error
|
|
251
|
+
except MCPError as error:
|
|
252
|
+
raise HttpError(422, str(error)) from error
|
|
253
|
+
except ValidationError as error:
|
|
254
|
+
raise HttpError(422, "The MCP connection configuration is invalid.") from error
|
|
255
|
+
return apply_approval(connection, payload.require_approval)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@router.post(
|
|
259
|
+
"/connections/api/inspect", response=APISpecSummarySchema, auth=django_auth
|
|
260
|
+
)
|
|
261
|
+
def inspect_api_spec(
|
|
262
|
+
request: HttpRequest, payload: APISpecSchema
|
|
263
|
+
) -> APISpecSummarySchema:
|
|
264
|
+
"""The spec's title, servers, and operation counts, before connecting."""
|
|
265
|
+
require(request, Action.ADD_API_CONNECTIONS)
|
|
266
|
+
try:
|
|
267
|
+
summary = summarize(payload.spec_url.strip(), payload.spec_text)
|
|
268
|
+
except APIError as error:
|
|
269
|
+
raise HttpError(422, str(error)) from error
|
|
270
|
+
return APISpecSummarySchema(
|
|
271
|
+
title=summary.title,
|
|
272
|
+
servers=list(summary.servers),
|
|
273
|
+
operations=summary.operations,
|
|
274
|
+
read_only=summary.read_only,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@router.post("/connections/api", response=ConnectionSchema, auth=django_auth)
|
|
279
|
+
def create_api(
|
|
280
|
+
request: HttpRequest, payload: APIConnectionCreateSchema
|
|
281
|
+
) -> ConnectionSchema:
|
|
282
|
+
workspace = require(request, Action.ADD_API_CONNECTIONS)
|
|
283
|
+
try:
|
|
284
|
+
connection = create_api_connection(
|
|
285
|
+
workspace,
|
|
286
|
+
payload.binding_key,
|
|
287
|
+
payload.display_name,
|
|
288
|
+
payload.spec_url.strip(),
|
|
289
|
+
payload.spec_text,
|
|
290
|
+
payload.base_url,
|
|
291
|
+
payload.auth,
|
|
292
|
+
payload.header.strip(),
|
|
293
|
+
payload.token,
|
|
294
|
+
)
|
|
295
|
+
except ConnectionConflict as error:
|
|
296
|
+
raise HttpError(409, str(error)) from error
|
|
297
|
+
except APIError as error:
|
|
298
|
+
raise HttpError(422, str(error)) from error
|
|
299
|
+
except ValidationError as error:
|
|
300
|
+
raise HttpError(422, "The API connection configuration is invalid.") from error
|
|
301
|
+
return apply_approval(connection, payload.require_approval)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@router.post(
|
|
305
|
+
"/connections/{connection_id}/api", response=ConnectionSchema, auth=django_auth
|
|
306
|
+
)
|
|
307
|
+
def update_api(
|
|
308
|
+
request: HttpRequest, connection_id: int, payload: APIConnectionUpdateSchema
|
|
309
|
+
) -> ConnectionSchema:
|
|
310
|
+
connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
|
|
311
|
+
try:
|
|
312
|
+
connection = update_api_connection(
|
|
313
|
+
connection,
|
|
314
|
+
payload.display_name,
|
|
315
|
+
payload.token,
|
|
316
|
+
payload.base_url,
|
|
317
|
+
payload.operations,
|
|
318
|
+
)
|
|
319
|
+
except ConnectionConflict as error:
|
|
320
|
+
raise HttpError(409, str(error)) from error
|
|
321
|
+
except APIError as error:
|
|
322
|
+
raise HttpError(422, str(error)) from error
|
|
323
|
+
except ValidationError as error:
|
|
324
|
+
raise HttpError(422, "The API connection configuration is invalid.") from error
|
|
325
|
+
return apply_approval(connection, payload.require_approval)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@router.post(
|
|
329
|
+
"/connections/mcp/inspect", response=MCPInspectResultSchema, auth=django_auth
|
|
330
|
+
)
|
|
331
|
+
def inspect_mcp(
|
|
332
|
+
request: HttpRequest, payload: MCPInspectSchema
|
|
333
|
+
) -> MCPInspectResultSchema:
|
|
334
|
+
"""Whether a server offers sign-in, and which scopes it lists."""
|
|
335
|
+
require(request, Action.ADD_MCP_CONNECTIONS)
|
|
336
|
+
try:
|
|
337
|
+
server = inspect_server(payload.provider, payload.settings)
|
|
338
|
+
except MCPError as error:
|
|
339
|
+
raise HttpError(422, str(error)) from error
|
|
340
|
+
if server is None:
|
|
341
|
+
return MCPInspectResultSchema(oauth=False, scopes=[])
|
|
342
|
+
return MCPInspectResultSchema(oauth=True, scopes=list(server.scopes_supported))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@router.post(
|
|
346
|
+
"/connections/mcp/oauth/start", response=MCPSignInStartSchema, auth=django_auth
|
|
347
|
+
)
|
|
348
|
+
def start_mcp_sign_in(
|
|
349
|
+
request: HttpRequest, payload: MCPSignInSchema
|
|
350
|
+
) -> MCPSignInStartSchema:
|
|
351
|
+
workspace = require(request, Action.ADD_MCP_CONNECTIONS)
|
|
352
|
+
connection = (
|
|
353
|
+
get_object_or_404(
|
|
354
|
+
workspace_connections(workspace.pk, Connection.Kind.MCP),
|
|
355
|
+
pk=payload.connection_id,
|
|
356
|
+
)
|
|
357
|
+
if payload.connection_id
|
|
358
|
+
else None
|
|
359
|
+
)
|
|
360
|
+
if connection is None and not payload.binding_key:
|
|
361
|
+
raise HttpError(422, "Enter a binding.")
|
|
362
|
+
try:
|
|
363
|
+
url = start_authorization(
|
|
364
|
+
workspace,
|
|
365
|
+
request.user,
|
|
366
|
+
connection.provider if connection else payload.provider,
|
|
367
|
+
payload.binding_key,
|
|
368
|
+
payload.display_name,
|
|
369
|
+
connection.settings if connection else payload.settings,
|
|
370
|
+
requested_scopes(connection) if connection else payload.scopes,
|
|
371
|
+
connection,
|
|
372
|
+
payload.require_approval,
|
|
373
|
+
)
|
|
374
|
+
except ConnectionConflict as error:
|
|
375
|
+
raise HttpError(409, str(error)) from error
|
|
376
|
+
except MCPError as error:
|
|
377
|
+
raise HttpError(422, str(error)) from error
|
|
378
|
+
return MCPSignInStartSchema(authorize_url=url)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@router.get("/connections/oauth/callback", auth=None, include_in_schema=False)
|
|
382
|
+
def finish_mcp_sign_in(request: HttpRequest) -> HttpResponseRedirect:
|
|
383
|
+
"""The browser lands here from the sign-in page. Always redirect back."""
|
|
384
|
+
back = f"{settings.FRONTEND_URL.rstrip('/')}/connections"
|
|
385
|
+
if not request.user.is_authenticated:
|
|
386
|
+
return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'invalid'})}")
|
|
387
|
+
try:
|
|
388
|
+
connection = complete_authorization(
|
|
389
|
+
request.user,
|
|
390
|
+
request.GET.get("state", "")[:200],
|
|
391
|
+
request.GET.get("code", "")[:2000],
|
|
392
|
+
request.GET.get("error", "")[:200],
|
|
393
|
+
)
|
|
394
|
+
# Only fixed codes go in the URL, so a crafted link cannot show any text.
|
|
395
|
+
except SignInError as error:
|
|
396
|
+
return HttpResponseRedirect(f"{back}?{urlencode({'oauth': error.code})}")
|
|
397
|
+
except ConnectionConflict:
|
|
398
|
+
return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'taken'})}")
|
|
399
|
+
except (MCPError, ValidationError):
|
|
400
|
+
return HttpResponseRedirect(f"{back}?{urlencode({'oauth': 'failed'})}")
|
|
401
|
+
approve_waiting_for(connection)
|
|
402
|
+
query = {"oauth": "connected", "capability": connection.binding_key}
|
|
403
|
+
return HttpResponseRedirect(f"{back}?{urlencode(query)}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@router.post("/connections/webhook", response=ConnectionSchema, auth=django_auth)
|
|
407
|
+
def create_webhook(
|
|
408
|
+
request: HttpRequest,
|
|
409
|
+
payload: WebhookConnectionCreateSchema,
|
|
410
|
+
) -> ConnectionSchema:
|
|
411
|
+
workspace = require(request, Action.ADD_CONNECTIONS)
|
|
412
|
+
try:
|
|
413
|
+
connection = create_webhook_connection(
|
|
414
|
+
workspace,
|
|
415
|
+
payload.binding_key,
|
|
416
|
+
payload.display_name,
|
|
417
|
+
payload.url,
|
|
418
|
+
)
|
|
419
|
+
except ConnectionConflict as error:
|
|
420
|
+
raise HttpError(409, str(error)) from error
|
|
421
|
+
except WebhookError as error:
|
|
422
|
+
raise HttpError(422, str(error)) from error
|
|
423
|
+
except ValidationError as error:
|
|
424
|
+
raise HttpError(422, " ".join(error.messages)) from error
|
|
425
|
+
return apply_approval(connection, payload.require_approval)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def apply_approval(
|
|
429
|
+
connection: Connection, require_approval: bool | None
|
|
430
|
+
) -> ConnectionSchema:
|
|
431
|
+
"""Save the owner's approval choice and approve requests it now allows."""
|
|
432
|
+
if require_approval is None:
|
|
433
|
+
approve_waiting_for(connection)
|
|
434
|
+
else:
|
|
435
|
+
set_require_approval(connection, require_approval)
|
|
436
|
+
return serialize_connection(connection)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def owned_connection(
|
|
440
|
+
request: HttpRequest, connection_id: int, action: Action
|
|
441
|
+
) -> Connection:
|
|
442
|
+
workspace = require(request, action)
|
|
443
|
+
return get_object_or_404(workspace_connections(workspace.pk), pk=connection_id)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@router.post(
|
|
447
|
+
"/connections/{connection_id}/webhook",
|
|
448
|
+
response=ConnectionSchema,
|
|
449
|
+
auth=django_auth,
|
|
450
|
+
)
|
|
451
|
+
def update_webhook(
|
|
452
|
+
request: HttpRequest,
|
|
453
|
+
connection_id: int,
|
|
454
|
+
payload: WebhookConnectionUpdateSchema,
|
|
455
|
+
) -> ConnectionSchema:
|
|
456
|
+
connection = owned_connection(request, connection_id, Action.EDIT_CONNECTIONS)
|
|
457
|
+
try:
|
|
458
|
+
connection = update_webhook_connection(
|
|
459
|
+
connection, payload.display_name, payload.url
|
|
460
|
+
)
|
|
461
|
+
except ConnectionConflict as error:
|
|
462
|
+
raise HttpError(409, str(error)) from error
|
|
463
|
+
except WebhookError as error:
|
|
464
|
+
raise HttpError(422, str(error)) from error
|
|
465
|
+
except ValidationError as error:
|
|
466
|
+
raise HttpError(422, " ".join(error.messages)) from error
|
|
467
|
+
return apply_approval(connection, payload.require_approval)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@router.post("/connections/model/models", response=list[str], auth=django_auth)
|
|
471
|
+
def discover_model_provider_models(
|
|
472
|
+
request: HttpRequest, payload: ModelConnectionCreateSchema
|
|
473
|
+
) -> list[str]:
|
|
474
|
+
require(request, Action.ADD_MODEL_PROVIDERS)
|
|
475
|
+
try:
|
|
476
|
+
connection = Connection(
|
|
477
|
+
provider=payload.provider,
|
|
478
|
+
endpoint_url=validated_endpoint(payload.provider, payload.base_url),
|
|
479
|
+
access_token=checked_api_key(payload.provider, payload.api_key),
|
|
480
|
+
)
|
|
481
|
+
return fetch_models(connection)
|
|
482
|
+
except (ModelProviderError, CheckFailed) as error:
|
|
483
|
+
raise HttpError(422, str(error)) from error
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@router.post(
|
|
487
|
+
"/connections/{connection_id}/model/models", response=list[str], auth=django_auth
|
|
488
|
+
)
|
|
489
|
+
def refresh_model_provider_models(
|
|
490
|
+
request: HttpRequest, connection_id: int, payload: ModelConnectionUpdateSchema
|
|
491
|
+
) -> list[str]:
|
|
492
|
+
connection = owned_connection(request, connection_id, Action.EDIT_MODEL_PROVIDERS)
|
|
493
|
+
if connection.kind != Connection.Kind.MODEL:
|
|
494
|
+
raise HttpError(422, "This is not a model provider connection.")
|
|
495
|
+
try:
|
|
496
|
+
# Preview changes without saving credentials, settings, or discovery results.
|
|
497
|
+
preview = Connection(
|
|
498
|
+
provider=connection.provider,
|
|
499
|
+
endpoint_url=validated_endpoint(
|
|
500
|
+
connection.provider,
|
|
501
|
+
payload.base_url
|
|
502
|
+
if payload.base_url is not None
|
|
503
|
+
else connection.endpoint_url,
|
|
504
|
+
),
|
|
505
|
+
access_token=payload.api_key.strip()
|
|
506
|
+
if payload.api_key and payload.api_key.strip()
|
|
507
|
+
else connection.access_token,
|
|
508
|
+
)
|
|
509
|
+
return fetch_models(preview)
|
|
510
|
+
except (ModelProviderError, CheckFailed) as error:
|
|
511
|
+
raise HttpError(422, str(error)) from error
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@router.post("/connections/model", response=ConnectionSchema, auth=django_auth)
|
|
515
|
+
def create_model_provider(
|
|
516
|
+
request: HttpRequest,
|
|
517
|
+
payload: ModelConnectionCreateSchema,
|
|
518
|
+
) -> ConnectionSchema:
|
|
519
|
+
workspace = require(request, Action.ADD_MODEL_PROVIDERS)
|
|
520
|
+
try:
|
|
521
|
+
connection = create_model_connection(
|
|
522
|
+
workspace,
|
|
523
|
+
payload.provider,
|
|
524
|
+
payload.display_name,
|
|
525
|
+
payload.api_key,
|
|
526
|
+
endpoint_url=payload.base_url,
|
|
527
|
+
allowed_models=payload.allowed_models,
|
|
528
|
+
default_model=payload.default_model,
|
|
529
|
+
)
|
|
530
|
+
except ModelProviderError as error:
|
|
531
|
+
raise HttpError(422, str(error)) from error
|
|
532
|
+
except ValidationError as error:
|
|
533
|
+
raise HttpError(422, " ".join(error.messages)) from error
|
|
534
|
+
apply_model_default(connection, payload.is_default)
|
|
535
|
+
return serialize_connection(connection)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@router.post(
|
|
539
|
+
"/connections/{connection_id}/model",
|
|
540
|
+
response=ConnectionSchema,
|
|
541
|
+
auth=django_auth,
|
|
542
|
+
)
|
|
543
|
+
def update_model_provider(
|
|
544
|
+
request: HttpRequest,
|
|
545
|
+
connection_id: int,
|
|
546
|
+
payload: ModelConnectionUpdateSchema,
|
|
547
|
+
) -> ConnectionSchema:
|
|
548
|
+
connection = owned_connection(request, connection_id, Action.EDIT_MODEL_PROVIDERS)
|
|
549
|
+
try:
|
|
550
|
+
connection = update_model_connection(
|
|
551
|
+
connection,
|
|
552
|
+
display_name=payload.display_name,
|
|
553
|
+
api_key=payload.api_key,
|
|
554
|
+
endpoint_url=payload.base_url,
|
|
555
|
+
allowed_models=payload.allowed_models,
|
|
556
|
+
default_model=payload.default_model,
|
|
557
|
+
)
|
|
558
|
+
except ModelProviderError as error:
|
|
559
|
+
raise HttpError(422, str(error)) from error
|
|
560
|
+
except ValidationError as error:
|
|
561
|
+
raise HttpError(422, " ".join(error.messages)) from error
|
|
562
|
+
apply_model_default(connection, payload.is_default)
|
|
563
|
+
return serialize_connection(connection)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@router.post(
|
|
567
|
+
"/connections/{connection_id}/revoke",
|
|
568
|
+
response=ConnectionSchema,
|
|
569
|
+
auth=django_auth,
|
|
570
|
+
)
|
|
571
|
+
def revoke(request: HttpRequest, connection_id: int) -> ConnectionSchema:
|
|
572
|
+
connection = owned_connection(request, connection_id, Action.DISCONNECT_CONNECTIONS)
|
|
573
|
+
return serialize_connection(revoke_connection(connection))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@router.delete(
|
|
577
|
+
"/connections/{connection_id}",
|
|
578
|
+
response={204: None},
|
|
579
|
+
auth=django_auth,
|
|
580
|
+
)
|
|
581
|
+
def remove_connection(request: HttpRequest, connection_id: int) -> Status[None]:
|
|
582
|
+
connection = owned_connection(request, connection_id, Action.DELETE_CONNECTIONS)
|
|
583
|
+
try:
|
|
584
|
+
delete_connection(connection)
|
|
585
|
+
except ConnectionConflict as error:
|
|
586
|
+
raise HttpError(409, str(error)) from error
|
|
587
|
+
return Status(204, None)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@router.post(
|
|
591
|
+
"/connections/{connection_id}/test",
|
|
592
|
+
response=ConnectionTestSchema,
|
|
593
|
+
auth=django_auth,
|
|
594
|
+
)
|
|
595
|
+
def test_connection(request: HttpRequest, connection_id: int) -> ConnectionTestSchema:
|
|
596
|
+
connection = owned_connection(request, connection_id, Action.TEST_CONNECTIONS)
|
|
597
|
+
if connection.kind == Connection.Kind.MCP:
|
|
598
|
+
try:
|
|
599
|
+
message = recheck_mcp_connection(connection)
|
|
600
|
+
except MCPError as error:
|
|
601
|
+
return ConnectionTestSchema(ok=False, status_code=None, message=str(error))
|
|
602
|
+
return ConnectionTestSchema(ok=True, status_code=None, message=message)
|
|
603
|
+
if connection.kind == Connection.Kind.API:
|
|
604
|
+
try:
|
|
605
|
+
message = recheck_api_connection(connection)
|
|
606
|
+
except APIError as error:
|
|
607
|
+
return ConnectionTestSchema(ok=False, status_code=None, message=str(error))
|
|
608
|
+
return ConnectionTestSchema(ok=True, status_code=None, message=message)
|
|
609
|
+
if connection.kind == Connection.Kind.MODEL:
|
|
610
|
+
check = check_model_connection(connection)
|
|
611
|
+
return ConnectionTestSchema(
|
|
612
|
+
ok=check.ok, status_code=None, message=check.message, models=check.models
|
|
613
|
+
)
|
|
614
|
+
if (
|
|
615
|
+
connection.kind == Connection.Kind.SOURCE
|
|
616
|
+
and connection.provider == Connection.Provider.GITHUB
|
|
617
|
+
):
|
|
618
|
+
check = check_source(connection)
|
|
619
|
+
return ConnectionTestSchema(
|
|
620
|
+
ok=check.ok, status_code=None, message=check.message
|
|
621
|
+
)
|
|
622
|
+
if connection.provider != Connection.Provider.WEBHOOK:
|
|
623
|
+
raise HttpError(400, "Testing is not available for this connection yet.")
|
|
624
|
+
delivery = deliver_webhook(connection, connection_test_payload(connection))
|
|
625
|
+
return ConnectionTestSchema(
|
|
626
|
+
ok=delivery.ok,
|
|
627
|
+
status_code=delivery.status_code,
|
|
628
|
+
message=delivery.message,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@router.get(
|
|
633
|
+
"/connections/webhook/sample", response=WebhookSampleSchema, auth=django_auth
|
|
634
|
+
)
|
|
635
|
+
def webhook_sample(request: HttpRequest) -> WebhookSampleSchema:
|
|
636
|
+
"""What a webhook connection will receive after each run."""
|
|
637
|
+
return WebhookSampleSchema(
|
|
638
|
+
payload=sample_run_report_payload(), summary_max_chars=SUMMARY_MAX_CHARS
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@router.get("/agent-connections", response=list[AgentConnectionSchema])
|
|
643
|
+
def agent_connections(request: HttpRequest) -> list[AgentConnection]:
|
|
644
|
+
workspace = current_workspace(request)
|
|
645
|
+
if not workspace:
|
|
646
|
+
return []
|
|
647
|
+
return with_recent_runs(workspace_agent_connections(workspace), workspace.pk)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@router.post("/agent-connections", response=AgentConnectionSchema)
|
|
651
|
+
def connect_agent(
|
|
652
|
+
request: HttpRequest, payload: AgentConnectionInput
|
|
653
|
+
) -> AgentConnection:
|
|
654
|
+
workspace = require(request, Action.ADD_CONNECTIONS)
|
|
655
|
+
try:
|
|
656
|
+
connection = save_agent_connection(
|
|
657
|
+
workspace,
|
|
658
|
+
request.user,
|
|
659
|
+
payload.backend,
|
|
660
|
+
payload.display_name,
|
|
661
|
+
payload.auth.get_secret_value(),
|
|
662
|
+
)
|
|
663
|
+
except AgentConnectionError as error:
|
|
664
|
+
raise HttpError(422, str(error)) from None
|
|
665
|
+
use_when_nothing_runs(workspace, login=connection)
|
|
666
|
+
return connection
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
@router.post("/agent-connections/{connection_id}", response=AgentConnectionSchema)
|
|
670
|
+
def reconnect_agent(
|
|
671
|
+
request: HttpRequest, connection_id: int, payload: AgentConnectionInput
|
|
672
|
+
) -> AgentConnection:
|
|
673
|
+
workspace = require(request, Action.EDIT_CONNECTIONS)
|
|
674
|
+
connection = get_object_or_404(
|
|
675
|
+
own_agent_connections(workspace, request.user), pk=connection_id
|
|
676
|
+
)
|
|
677
|
+
try:
|
|
678
|
+
return reconnect_agent_connection(
|
|
679
|
+
connection,
|
|
680
|
+
request.user,
|
|
681
|
+
payload.backend,
|
|
682
|
+
payload.display_name,
|
|
683
|
+
payload.auth.get_secret_value(),
|
|
684
|
+
)
|
|
685
|
+
except AgentConnectionError as error:
|
|
686
|
+
raise HttpError(422, str(error)) from None
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@router.post("/agent-connections/{connection_id}/check", response=AgentConnectionSchema)
|
|
690
|
+
def check_agent(request: HttpRequest, connection_id: int) -> AgentConnection:
|
|
691
|
+
"""Only the login's owner checks it. The check uses their plan's allowance."""
|
|
692
|
+
workspace = require(request, Action.TEST_CONNECTIONS)
|
|
693
|
+
connection = get_object_or_404(
|
|
694
|
+
own_agent_connections(workspace, request.user), pk=connection_id
|
|
695
|
+
)
|
|
696
|
+
try:
|
|
697
|
+
check_agent_connection(connection)
|
|
698
|
+
except AgentConnectionError as error:
|
|
699
|
+
raise HttpError(409, str(error)) from None
|
|
700
|
+
return connection
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
@router.post(
|
|
704
|
+
"/agent-connections/{connection_id}/run-limit", response=AgentConnectionSchema
|
|
705
|
+
)
|
|
706
|
+
def set_agent_run_limit(
|
|
707
|
+
request: HttpRequest, connection_id: int, payload: AgentRunLimitInput
|
|
708
|
+
) -> AgentConnection:
|
|
709
|
+
"""Only the login's owner sets its cap. It is their subscription."""
|
|
710
|
+
workspace = require(request, Action.EDIT_CONNECTIONS)
|
|
711
|
+
connection = get_object_or_404(
|
|
712
|
+
own_agent_connections(workspace, request.user), pk=connection_id
|
|
713
|
+
)
|
|
714
|
+
connection.max_runs_per_day = payload.max_runs_per_day
|
|
715
|
+
connection.save(update_fields=["max_runs_per_day", "updated_at"])
|
|
716
|
+
return connection
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@router.delete("/agent-connections/{connection_id}", response={204: None})
|
|
720
|
+
def disconnect_agent(request: HttpRequest, connection_id: int) -> Status[None]:
|
|
721
|
+
workspace = require(request, Action.DISCONNECT_CONNECTIONS)
|
|
722
|
+
connection = get_object_or_404(
|
|
723
|
+
own_agent_connections(workspace, request.user), pk=connection_id
|
|
724
|
+
)
|
|
725
|
+
# A cached copy the runner keeps is removed by the worker.
|
|
726
|
+
disconnect_agent_connection(connection)
|
|
727
|
+
return Status(204, None)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@router.get("/settings/agent", response=ExecutionConfigurationSchema)
|
|
731
|
+
def default_agent(request: HttpRequest) -> dict[str, Any]:
|
|
732
|
+
workspace = current_workspace(request)
|
|
733
|
+
if workspace is None:
|
|
734
|
+
raise HttpError(404, "Choose a workspace first.")
|
|
735
|
+
return execution_configuration(workspace, request.user)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@router.post("/settings/agent", response=ExecutionConfigurationSchema)
|
|
739
|
+
def set_default_agent(
|
|
740
|
+
request: HttpRequest, payload: ExecutionPreferenceSchema
|
|
741
|
+
) -> dict[str, Any]:
|
|
742
|
+
workspace = require(request, Action.REVIEW_CAPABILITIES)
|
|
743
|
+
try:
|
|
744
|
+
save_execution_preference(workspace, payload.key, request.user)
|
|
745
|
+
except ValidationError as error:
|
|
746
|
+
raise HttpError(422, error.messages[0]) from None
|
|
747
|
+
return execution_configuration(workspace, request.user)
|