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,94 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.contrib.auth.models import AbstractBaseUser
|
|
3
|
+
from django.db import transaction
|
|
4
|
+
from django.utils.text import slugify
|
|
5
|
+
|
|
6
|
+
from secondshift.workspaces.access import ROLE_RANK, Role
|
|
7
|
+
from secondshift.workspaces.dao import (
|
|
8
|
+
create_workspace,
|
|
9
|
+
lock_other_owners_exist,
|
|
10
|
+
owned_workspace_count,
|
|
11
|
+
slug_taken,
|
|
12
|
+
workspace_members,
|
|
13
|
+
)
|
|
14
|
+
from secondshift.workspaces.models import Workspace, WorkspaceMembership
|
|
15
|
+
|
|
16
|
+
LAST_OWNER_MESSAGE = "A workspace needs at least one owner."
|
|
17
|
+
# How many workspaces someone who is not staff may own, so open sign-up
|
|
18
|
+
# cannot be used to fill the database.
|
|
19
|
+
OWNED_WORKSPACE_LIMIT = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MembershipError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WorkspaceCreationError(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkspaceCreationRefused(WorkspaceCreationError):
|
|
31
|
+
"""The person may not create one, as opposed to asking for a bad one."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def can_create_workspace(user: AbstractBaseUser) -> bool:
|
|
35
|
+
"""Anyone may when sign-up is open. Otherwise only staff, who run the server."""
|
|
36
|
+
return bool(settings.SIGNUP_ENABLED or getattr(user, "is_staff", False))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def free_slug(name: str) -> str:
|
|
40
|
+
"""A slug from the name, with a number added when it is taken."""
|
|
41
|
+
base = slugify(name)[:40].strip("-") or "workspace"
|
|
42
|
+
slug, number = base, 1
|
|
43
|
+
while slug_taken(slug):
|
|
44
|
+
number += 1
|
|
45
|
+
slug = f"{base}-{number}"
|
|
46
|
+
return slug
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_workspace_for(user: AbstractBaseUser, name: str) -> Workspace:
|
|
50
|
+
"""A workspace someone made for themselves from the dashboard."""
|
|
51
|
+
if not can_create_workspace(user):
|
|
52
|
+
raise WorkspaceCreationRefused(
|
|
53
|
+
"Only server staff can create workspaces. Ask an owner for an invite."
|
|
54
|
+
)
|
|
55
|
+
name = name.strip()
|
|
56
|
+
if not name:
|
|
57
|
+
raise WorkspaceCreationError("Give the workspace a name.")
|
|
58
|
+
owned = owned_workspace_count(user)
|
|
59
|
+
if not getattr(user, "is_staff", False) and owned >= OWNED_WORKSPACE_LIMIT:
|
|
60
|
+
raise WorkspaceCreationRefused(
|
|
61
|
+
f"You can own up to {OWNED_WORKSPACE_LIMIT} workspaces."
|
|
62
|
+
)
|
|
63
|
+
return create_workspace(name, free_slug(name), user)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def members_in_order(workspace: Workspace) -> list[WorkspaceMembership]:
|
|
67
|
+
"""Owners first, then admins, then members, each in the order they joined."""
|
|
68
|
+
return sorted(
|
|
69
|
+
workspace_members(workspace),
|
|
70
|
+
key=lambda membership: -ROLE_RANK[Role(membership.role)],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def ensure_another_owner(membership: WorkspaceMembership) -> None:
|
|
75
|
+
"""Refuse to take away the workspace's last owner."""
|
|
76
|
+
if membership.role != Role.OWNER:
|
|
77
|
+
return
|
|
78
|
+
if not lock_other_owners_exist(membership):
|
|
79
|
+
raise MembershipError(LAST_OWNER_MESSAGE)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@transaction.atomic
|
|
83
|
+
def change_role(membership: WorkspaceMembership, role: str) -> WorkspaceMembership:
|
|
84
|
+
if Role(role) != Role.OWNER:
|
|
85
|
+
ensure_another_owner(membership)
|
|
86
|
+
membership.role = Role(role)
|
|
87
|
+
membership.save(update_fields=["role"])
|
|
88
|
+
return membership
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@transaction.atomic
|
|
92
|
+
def remove_member(membership: WorkspaceMembership) -> None:
|
|
93
|
+
ensure_another_owner(membership)
|
|
94
|
+
membership.delete()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""API tokens: how MCP clients sign in as a person in one workspace.
|
|
2
|
+
|
|
3
|
+
A token is shown once, when it is created. The database keeps a hash of it.
|
|
4
|
+
A token works until it expires or is revoked, and only while its creator
|
|
5
|
+
still belongs to its workspace. Its scope limits what it can change. See
|
|
6
|
+
TOKEN_ACTIONS in secondshift.workspaces.access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import secrets
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
|
|
14
|
+
from django.conf import settings
|
|
15
|
+
from django.http import HttpRequest
|
|
16
|
+
from django.utils import timezone
|
|
17
|
+
from ninja.security import HttpBearer
|
|
18
|
+
|
|
19
|
+
from secondshift.workspaces.access import (
|
|
20
|
+
API_TOKEN_ATTRIBUTE,
|
|
21
|
+
MEMBERSHIP_ATTRIBUTE,
|
|
22
|
+
TOKEN_ACTIONS,
|
|
23
|
+
role_allows,
|
|
24
|
+
)
|
|
25
|
+
from secondshift.workspaces.dao import (
|
|
26
|
+
api_token_by_hash,
|
|
27
|
+
create_token_record,
|
|
28
|
+
touch_token,
|
|
29
|
+
)
|
|
30
|
+
from secondshift.workspaces.models import ApiToken, WorkspaceMembership
|
|
31
|
+
|
|
32
|
+
# Secret scanners can match tokens by this prefix.
|
|
33
|
+
TOKEN_PREFIX = "ss_"
|
|
34
|
+
# Last use is saved at most this often, so each call does not write.
|
|
35
|
+
LAST_USE_PRECISION = timedelta(minutes=1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TokenError(Exception):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class NewApiToken:
|
|
44
|
+
token: ApiToken
|
|
45
|
+
# Only here, right after creating it. It is never stored.
|
|
46
|
+
secret: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def token_hash(secret: str) -> str:
|
|
50
|
+
return hashlib.sha256(secret.encode()).hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def scope_allowed(role: str, scope: str) -> bool:
|
|
54
|
+
"""Whether a role may create a token with this scope."""
|
|
55
|
+
return all(
|
|
56
|
+
role_allows(role, action) for action in TOKEN_ACTIONS[ApiToken.Scope(scope)]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_api_token(
|
|
61
|
+
membership: WorkspaceMembership, name: str, scope: str, days: int
|
|
62
|
+
) -> NewApiToken:
|
|
63
|
+
if not 1 <= days <= settings.API_TOKEN_MAX_DAYS:
|
|
64
|
+
raise TokenError(
|
|
65
|
+
f"Choose an expiry between 1 and {settings.API_TOKEN_MAX_DAYS} days."
|
|
66
|
+
)
|
|
67
|
+
if not scope_allowed(membership.role, scope):
|
|
68
|
+
raise TokenError("Only workspace owners and admins can create run tokens.")
|
|
69
|
+
secret = TOKEN_PREFIX + secrets.token_urlsafe(32)
|
|
70
|
+
token = create_token_record(
|
|
71
|
+
membership,
|
|
72
|
+
name,
|
|
73
|
+
ApiToken.Scope(scope),
|
|
74
|
+
token_hash(secret),
|
|
75
|
+
timezone.now() + timedelta(days=days),
|
|
76
|
+
)
|
|
77
|
+
return NewApiToken(token, secret)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def revoke_api_token(token: ApiToken) -> None:
|
|
81
|
+
token.revoked_at = timezone.now()
|
|
82
|
+
token.save(update_fields=["revoked_at"])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_api_token(secret: str) -> ApiToken | None:
|
|
86
|
+
"""The token for a secret while it works: usable, creator still a member."""
|
|
87
|
+
return api_token_by_hash(token_hash(secret))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def record_use(token: ApiToken) -> None:
|
|
91
|
+
now = timezone.now()
|
|
92
|
+
touch_token(token, now, now - LAST_USE_PRECISION)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ApiTokenAuth(HttpBearer):
|
|
96
|
+
"""Sign the request in as the token's creator, in the token's workspace."""
|
|
97
|
+
|
|
98
|
+
def authenticate(self, request: HttpRequest, token: str) -> ApiToken | None:
|
|
99
|
+
api_token = find_api_token(token)
|
|
100
|
+
if api_token is None:
|
|
101
|
+
return None
|
|
102
|
+
request.user = api_token.created_by
|
|
103
|
+
setattr(request, API_TOKEN_ATTRIBUTE, api_token)
|
|
104
|
+
# Forget any membership resolved from the session before this point.
|
|
105
|
+
if hasattr(request, MEMBERSHIP_ATTRIBUTE):
|
|
106
|
+
delattr(request, MEMBERSHIP_ATTRIBUTE)
|
|
107
|
+
record_use(api_token)
|
|
108
|
+
return api_token
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|
2
|
+
from django.shortcuts import render
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
from django.views.decorators.cache import never_cache
|
|
5
|
+
from django.views.decorators.http import require_GET
|
|
6
|
+
|
|
7
|
+
from secondshift.workspaces.invites import (
|
|
8
|
+
INVITE_UNUSABLE_MESSAGE,
|
|
9
|
+
find_invite,
|
|
10
|
+
remember_invite,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@never_cache
|
|
15
|
+
@require_GET
|
|
16
|
+
def open_invite(request: HttpRequest, token: str) -> HttpResponse:
|
|
17
|
+
"""Remember the invite, then send the person to sign up or to the app.
|
|
18
|
+
|
|
19
|
+
Opening a link never joins a workspace. The dashboard asks the signed-in
|
|
20
|
+
person to confirm first.
|
|
21
|
+
"""
|
|
22
|
+
if find_invite(token) is None:
|
|
23
|
+
return render(
|
|
24
|
+
request,
|
|
25
|
+
"workspaces/invite_unusable.html",
|
|
26
|
+
{"message": INVITE_UNUSABLE_MESSAGE},
|
|
27
|
+
status=400,
|
|
28
|
+
)
|
|
29
|
+
remember_invite(request, token)
|
|
30
|
+
if request.user.is_authenticated:
|
|
31
|
+
return HttpResponseRedirect("/")
|
|
32
|
+
return HttpResponseRedirect(reverse("account_signup"))
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: secondshift
|
|
3
|
+
Version: 0.1.0b12
|
|
4
|
+
Summary: Unattended agent jobs for your team, defined in Git.
|
|
5
|
+
Project-URL: Homepage, https://second-shift.app/
|
|
6
|
+
Project-URL: Documentation, https://second-shift.app/docs/overview/
|
|
7
|
+
Project-URL: Repository, https://github.com/piotrekno1/second-shift
|
|
8
|
+
Project-URL: Issues, https://github.com/piotrekno1/second-shift/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/piotrekno1/second-shift/releases
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Python: <3.14,>=3.13
|
|
13
|
+
Requires-Dist: anthropic>=1.15.0
|
|
14
|
+
Requires-Dist: anyio>=4.15.1
|
|
15
|
+
Requires-Dist: certifi>=2026.10.19
|
|
16
|
+
Requires-Dist: croniter>=6.0.0
|
|
17
|
+
Requires-Dist: cryptography>=46.0.0
|
|
18
|
+
Requires-Dist: dbos>=3.3.0
|
|
19
|
+
Requires-Dist: django-allauth>=65.0.0
|
|
20
|
+
Requires-Dist: django-axes>=8.3.1
|
|
21
|
+
Requires-Dist: django-click>=2.4.0
|
|
22
|
+
Requires-Dist: django-cors-headers>=4.9.0
|
|
23
|
+
Requires-Dist: django-encrypted-model-fields>=0.6.5
|
|
24
|
+
Requires-Dist: django-environ>=0.13.0
|
|
25
|
+
Requires-Dist: django-health-check>=4.8.0
|
|
26
|
+
Requires-Dist: django-ninja>=1.5.0
|
|
27
|
+
Requires-Dist: django-storages[s3]>=1.14.6
|
|
28
|
+
Requires-Dist: django<6.1,>=6.0
|
|
29
|
+
Requires-Dist: genai-prices>=0.1.13
|
|
30
|
+
Requires-Dist: gunicorn>=23.0.0
|
|
31
|
+
Requires-Dist: httpcore>=1.0.9
|
|
32
|
+
Requires-Dist: httpx>=0.28.1
|
|
33
|
+
Requires-Dist: jsonschema<5,>=4.26
|
|
34
|
+
Requires-Dist: mcp<2,>=1.26
|
|
35
|
+
Requires-Dist: psycopg[binary,pool]>=3.2.0
|
|
36
|
+
Requires-Dist: pydantic-ai-slim[anthropic,dbos,openai]>=2.57.0
|
|
37
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
38
|
+
Requires-Dist: redis>=6
|
|
39
|
+
Requires-Dist: thin-mcp[django]<0.3,>=0.2
|
|
40
|
+
Requires-Dist: uvicorn-worker>=0.4.0
|
|
41
|
+
Requires-Dist: whitenoise>=6.11.0
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# SecondShift
|
|
45
|
+
|
|
46
|
+
**Unattended agent jobs for your team, without giving the agent your service
|
|
47
|
+
credentials.**
|
|
48
|
+
|
|
49
|
+
Define a job once in your repository. SecondShift runs it on a schedule, when
|
|
50
|
+
an alert or other webhook arrives, or when anyone on the team asks from the
|
|
51
|
+
dashboard or their coding agent. The agent works in a sandbox with no network
|
|
52
|
+
and no service credentials. Every tool call goes through a broker that checks
|
|
53
|
+
it against what a workspace owner approved. Results land where your team
|
|
54
|
+
already works, such as Slack, a GitHub issue or a draft PR.
|
|
55
|
+
|
|
56
|
+
Built for platform, security and SRE teams who want agents doing recurring
|
|
57
|
+
work without handing them the keys. Runs on your laptop with one command, or
|
|
58
|
+
on your own server in production.
|
|
59
|
+
|
|
60
|
+
[Quick start](#quick-start) · [Skill catalog](examples/README.md) ·
|
|
61
|
+
[Security model](docs/security-model.md) ·
|
|
62
|
+
[Docs](https://second-shift.app/docs/overview/)
|
|
63
|
+
|
|
64
|
+
https://github.com/user-attachments/assets/69cbc92f-72f6-45fb-98ad-27b4b1056581
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
You need [uv](https://docs.astral.sh/uv/getting-started/installation/), Docker,
|
|
69
|
+
and a model API key or a Claude Code or Codex subscription. Start Docker, then
|
|
70
|
+
run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
release=https://github.com/piotrekno1/second-shift/releases
|
|
74
|
+
uv tool install --force --python 3.13 \
|
|
75
|
+
"$release/download/v0.1.0b12/secondshift-0.1.0b12-py3-none-any.whl"
|
|
76
|
+
secondshift up
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The first start builds the agent image for subscriptions and takes a few
|
|
80
|
+
minutes. Use `secondshift up --no-agents` to skip it. Your dashboard opens when
|
|
81
|
+
SecondShift is ready.
|
|
82
|
+
|
|
83
|
+
Try it on this repository. SecondShift reviews its own documentation:
|
|
84
|
+
|
|
85
|
+
1. Clone it: `git clone https://github.com/piotrekno1/second-shift.git`
|
|
86
|
+
2. In **Repositories**, choose **Add repository**, then **Local folder**, and
|
|
87
|
+
select the clone.
|
|
88
|
+
3. In **Models**, add an API key or a Claude Code or Codex subscription.
|
|
89
|
+
4. In **Jobs**, open **Documentation drift** and click **Run now**.
|
|
90
|
+
|
|
91
|
+
For one-off work, open a repository, click **New task** and write what to do.
|
|
92
|
+
To connect your coding agent, open **Connect your agent** in the account menu.
|
|
93
|
+
|
|
94
|
+
### Try a skill from GitHub in the sandbox
|
|
95
|
+
|
|
96
|
+
A skill from the internet is someone else's instructions, and many come with
|
|
97
|
+
scripts. Run one in your own coding agent and those scripts can reach whatever
|
|
98
|
+
your machine can. In SecondShift its commands run in the sandbox, with no
|
|
99
|
+
network and no credentials.
|
|
100
|
+
|
|
101
|
+
Open a repository, click **New task**, then **Skill from GitHub**, and paste a
|
|
102
|
+
link to a `SKILL.md`, such as one from
|
|
103
|
+
[anthropics/skills](https://github.com/anthropics/skills). SecondShift pins it
|
|
104
|
+
to a commit, shows what it asks for, and mounts it read-only. If you like the
|
|
105
|
+
result, click **Run again** or **Schedule this skill**. See
|
|
106
|
+
[Try a skill from GitHub](docs/tasks.md#try-a-skill-from-github).
|
|
107
|
+
|
|
108
|
+
## The problem
|
|
109
|
+
|
|
110
|
+
Your team asks agents for the same work every week. Triage the CI failures.
|
|
111
|
+
Check which docs went stale. Look into the alert that fired at 3am. Each time,
|
|
112
|
+
someone writes the prompt again, waits for the answer and pays for the tokens.
|
|
113
|
+
Often two people pay for the same answer.
|
|
114
|
+
|
|
115
|
+
Automating it means letting agents run unattended. The usual way is a cron
|
|
116
|
+
workflow in CI. The agent runs on the runner that holds your keys, with open
|
|
117
|
+
network access. One prompt injection in a log line or an issue reaches
|
|
118
|
+
everything that runner can. Token spend has no ceiling, and each repository
|
|
119
|
+
wires up its own workflow.
|
|
120
|
+
|
|
121
|
+
So teams keep prompting by hand, or accept these risks.
|
|
122
|
+
|
|
123
|
+
With SecondShift you define the work once. It runs on a schedule or when an
|
|
124
|
+
event arrives, once for the whole team. The agent never holds your service
|
|
125
|
+
credentials, and it can call only the tools an owner approved. The latest result is
|
|
126
|
+
already waiting in Slack, or in your coding agent over MCP.
|
|
127
|
+
|
|
128
|
+
## Who it's for
|
|
129
|
+
|
|
130
|
+
Platform, security and SRE teams at companies that already use agents and now
|
|
131
|
+
want them to run unattended. SecondShift fits when:
|
|
132
|
+
|
|
133
|
+
- your security review won't let an agent hold production credentials,
|
|
134
|
+
- several teams want recurring jobs with one set of approvals and one record,
|
|
135
|
+
- you need to choose the model or keep data on your own servers.
|
|
136
|
+
|
|
137
|
+
For a few personal jobs, Claude Code scheduled tasks or a GitHub Actions
|
|
138
|
+
workflow may be enough. See [Alternatives](docs/comparisons.md).
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
A job is a skill plus a few lines of YAML in `.secondshift.yaml`:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
jobs:
|
|
146
|
+
alert-triage:
|
|
147
|
+
name: Alert triage
|
|
148
|
+
skill: .agents/skills/alert-triage/SKILL.md
|
|
149
|
+
triggers: [webhook]
|
|
150
|
+
webhook:
|
|
151
|
+
skip_when: { status: resolved }
|
|
152
|
+
repeat_key: groupKey
|
|
153
|
+
repeat_window: 1h
|
|
154
|
+
requires:
|
|
155
|
+
mcps:
|
|
156
|
+
- binding: grafana
|
|
157
|
+
tools: [query_prometheus, query_loki_logs, alerting_rules_read]
|
|
158
|
+
- binding: sentry
|
|
159
|
+
tools: [search_issues, search_errors]
|
|
160
|
+
notify:
|
|
161
|
+
- binding: alerts-slack
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
1. **Merge it.** SecondShift syncs the repository and finds the job.
|
|
165
|
+
2. **Approve access.** The job asks for tools by name. A workspace owner
|
|
166
|
+
decides which connection provides them and can restrict their arguments.
|
|
167
|
+
The YAML never holds a credential, endpoint or token.
|
|
168
|
+
3. **It runs.** On a schedule, from a webhook, or on demand. The agent works in
|
|
169
|
+
a networkless sandbox on a snapshot of the commit. Tool calls go through a
|
|
170
|
+
broker that checks each one against the approved plan.
|
|
171
|
+
4. **It delivers.** SecondShift posts the report, opens the issues or the draft
|
|
172
|
+
pull request, and keeps the run's full record.
|
|
173
|
+
|
|
174
|
+
## What you get
|
|
175
|
+
|
|
176
|
+
**Work defined in Git.** Skills and schedules are files in the repository.
|
|
177
|
+
Changes go through pull requests and code owners. Every run records the commit
|
|
178
|
+
it ran and the exact access it had.
|
|
179
|
+
|
|
180
|
+
**Access an owner approves.** Repositories request capabilities. Owners grant
|
|
181
|
+
them per job, per tool, and can fix, limit or forbid individual arguments. An
|
|
182
|
+
owner can also make the merge itself the approval for tools a connection
|
|
183
|
+
already allows.
|
|
184
|
+
|
|
185
|
+
**No secrets in the sandbox.** The agent runs with no network and no
|
|
186
|
+
credentials. SecondShift makes tool calls, opens pull requests and files
|
|
187
|
+
issues on its behalf. The model is always isolated from keys.
|
|
188
|
+
|
|
189
|
+
**Three ways to start work.**
|
|
190
|
+
- *Schedules* for recurring reviews and briefings.
|
|
191
|
+
- *Webhooks* for events such as alerts, with signed requests and filters for
|
|
192
|
+
resolved and repeated alerts.
|
|
193
|
+
- *One-off tasks* from the dashboard or your coding agent, queued per
|
|
194
|
+
repository and optionally delayed. A task can run a skill from any public
|
|
195
|
+
GitHub repository, pinned to a commit.
|
|
196
|
+
|
|
197
|
+
**Output your team can act on.** Reports to Slack or any webhook, with retry.
|
|
198
|
+
Files the agent saved, such as CSVs and images. GitHub issues, deduplicated and
|
|
199
|
+
capped per run. Draft pull requests on a `secondshift/run-<id>` branch. Jobs
|
|
200
|
+
can read their own earlier reports, so a weekly review knows what it said last
|
|
201
|
+
week.
|
|
202
|
+
|
|
203
|
+
**Your choice of model.** Use an API key from a model provider, or a local
|
|
204
|
+
model. You can also run jobs through the official Claude Code or Codex CLI,
|
|
205
|
+
signed in with your own account. Check that your plan's terms cover it. For
|
|
206
|
+
team jobs in production, use an API key.
|
|
207
|
+
|
|
208
|
+
**Costs with a ceiling.** Each run has a token budget and a tool-call limit.
|
|
209
|
+
The gateway cuts a run off when its budget is used.
|
|
210
|
+
|
|
211
|
+
**History you can see.** Each run shows its trigger, commit, tool calls, token
|
|
212
|
+
usage and model cost. Runs are durable workflows. They survive
|
|
213
|
+
restarts, and retries don't repeat tool calls.
|
|
214
|
+
|
|
215
|
+
**Works with your coding agent.** SecondShift is an MCP server. Claude Code,
|
|
216
|
+
Codex, Cursor and other clients can check a `.secondshift.yaml` before you
|
|
217
|
+
commit it, start approved jobs, submit tasks and read reports. Tokens are
|
|
218
|
+
scoped to a person and a workspace and can never approve access.
|
|
219
|
+
|
|
220
|
+
## What teams run
|
|
221
|
+
|
|
222
|
+
The [skill catalog](examples/README.md) has example skills to copy into your
|
|
223
|
+
repository and adapt.
|
|
224
|
+
|
|
225
|
+
| Team | Examples |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| Developers | [Repository health](examples/repository-health/README.md), [docs drift](examples/docs-drift/README.md), [implicit state machines](examples/implicit-state-machines/README.md), [CI failures](examples/ci-failure-review/README.md) |
|
|
228
|
+
| SRE and operations | [Alert triage](examples/alert-triage/README.md), [production errors](examples/production-error-review/README.md), [monitoring coverage](examples/monitoring-coverage-review/README.md), [on-call coverage](examples/on-call-coverage/README.md) |
|
|
229
|
+
| Security and compliance | [Security regression](examples/security-regression/README.md), [quarterly access review](examples/quarterly-access-review/README.md) |
|
|
230
|
+
| Engineering management | [Monday briefing](examples/engineering-briefing/README.md), [release readiness](examples/release-readiness-review/README.md), [ticket reconciliation](examples/ticket-reconciliation/README.md) |
|
|
231
|
+
|
|
232
|
+
## Run it in production
|
|
233
|
+
|
|
234
|
+
The [deployment guide](docs/production-deployment.md) covers a single-server
|
|
235
|
+
Docker Compose setup with HTTPS, PostgreSQL, S3 or local artifact storage, and
|
|
236
|
+
gVisor. It includes tested backup, restore and upgrade steps.
|
|
237
|
+
|
|
238
|
+
See [GitHub repository access](docs/github-app.md) to set up access with a
|
|
239
|
+
GitHub App or a personal access token.
|
|
240
|
+
|
|
241
|
+
## Security
|
|
242
|
+
|
|
243
|
+
| What | Where it lives during a run | Never reaches |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| Model provider key | Gateway | Agent loop, sandbox, model |
|
|
246
|
+
| MCP and API credentials | Worker or gateway, per broker call | Sandbox, model |
|
|
247
|
+
| GitHub write token | Worker, after the sandbox stops | Sandbox, model |
|
|
248
|
+
| Subscription login | Runner, sealed to its key | Container arguments, environment, logs |
|
|
249
|
+
|
|
250
|
+
SecondShift treats repository content, model output and tool results as
|
|
251
|
+
untrusted.
|
|
252
|
+
|
|
253
|
+
Approving a write tool means trusting the agent with it. The
|
|
254
|
+
[security model](docs/security-model.md) covers this and the other risks that
|
|
255
|
+
remain.
|
|
256
|
+
|
|
257
|
+
## Status
|
|
258
|
+
|
|
259
|
+
SecondShift is in beta. See the [release notes](docs/releases/) for what
|
|
260
|
+
changed and what was tested.
|
|
261
|
+
|
|
262
|
+
## Learn more
|
|
263
|
+
|
|
264
|
+
- [Repository configuration](docs/repository-configuration.md)
|
|
265
|
+
- [Tasks](docs/tasks.md)
|
|
266
|
+
- [Agent subscriptions](docs/agent-subscriptions.md)
|
|
267
|
+
- [MCP server](docs/mcp.md) and [vendor MCP servers](docs/mcp-recipes.md)
|
|
268
|
+
- [Architecture](docs/architecture.md)
|
|
269
|
+
- [Alternatives](docs/comparisons.md)
|
|
270
|
+
- [Contributing](CONTRIBUTING.md)
|