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,261 @@
|
|
|
1
|
+
"""Run inputs and outputs.
|
|
2
|
+
|
|
3
|
+
Full runner output lives in the private ``run_artifacts`` storage, never in
|
|
4
|
+
Postgres or DBOS checkpoints. The database keeps metadata and a short excerpt.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import tarfile
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from datetime import UTC
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import IO, Any
|
|
14
|
+
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django.core.files.base import ContentFile, File
|
|
17
|
+
from django.core.files.storage import FileSystemStorage, Storage, storages
|
|
18
|
+
|
|
19
|
+
from secondshift.config.constants import (
|
|
20
|
+
MAX_CHANGES_BYTES,
|
|
21
|
+
MAX_OUTPUT_BYTES,
|
|
22
|
+
MAX_OUTPUT_FILES,
|
|
23
|
+
)
|
|
24
|
+
from secondshift.runner.collection import (
|
|
25
|
+
CHANGES_NAME,
|
|
26
|
+
MANIFEST_NAME,
|
|
27
|
+
MAX_LISTED_CHANGES,
|
|
28
|
+
OUTPUTS_DIR,
|
|
29
|
+
HashingReader,
|
|
30
|
+
safe_output_name,
|
|
31
|
+
)
|
|
32
|
+
from secondshift.runs.inputs import Payload
|
|
33
|
+
from secondshift.runs.models import Run
|
|
34
|
+
from secondshift.runs.outputs import Changes, RunOutputs, SavedFile, SkippedFile
|
|
35
|
+
|
|
36
|
+
TRUNCATION_MARKER = "\n\n[... {omitted} bytes omitted by SecondShift ...]\n\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class StoredOutput:
|
|
41
|
+
path: str
|
|
42
|
+
size_bytes: int
|
|
43
|
+
sha256: str
|
|
44
|
+
truncated: bool
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> dict[str, Any]:
|
|
47
|
+
return asdict(self)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def artifact_storage() -> Storage:
|
|
51
|
+
return storages["run_artifacts"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_artifact_prefix(run: Run) -> str:
|
|
55
|
+
"""Partition by workspace, then by the run's creation day (UTC).
|
|
56
|
+
|
|
57
|
+
A workspace's artifacts share one prefix, so deleting a tenant is one
|
|
58
|
+
prefix delete; within it, age-based sweeps can drop whole day folders. The
|
|
59
|
+
path is derived from stored fields only, so DBOS retries rewrite the same
|
|
60
|
+
object instead of creating a new one.
|
|
61
|
+
"""
|
|
62
|
+
created = run.created_at.astimezone(UTC)
|
|
63
|
+
return f"runs/{run.workspace_id}/{created:%Y/%m/%d}/{run.pk}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def output_path(run: Run) -> str:
|
|
67
|
+
return f"{run_artifact_prefix(run)}/output.log"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def input_path(run: Run) -> str:
|
|
71
|
+
return f"{run_artifact_prefix(run)}/input.md"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def payload_path(run: Run) -> str:
|
|
75
|
+
return f"{run_artifact_prefix(run)}/payload.txt"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CollectionError(ValueError):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def changes_path(run: Run) -> str:
|
|
83
|
+
return f"{run_artifact_prefix(run)}/{CHANGES_NAME}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def saved_file_path(run: Run, name: str) -> str:
|
|
87
|
+
return f"{run_artifact_prefix(run)}/{OUTPUTS_DIR}/{name}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def store_collection(run: Run, archive_path: Path) -> RunOutputs:
|
|
91
|
+
"""Keep what the runner collected, and return what Run.outputs records.
|
|
92
|
+
|
|
93
|
+
The archive comes from the trusted runner, but the agent chose its file
|
|
94
|
+
names and contents. Only the expected names and regular files are kept,
|
|
95
|
+
within the same limits the runner applies.
|
|
96
|
+
"""
|
|
97
|
+
stored_changes: dict[str, Any] = {}
|
|
98
|
+
files: list[SavedFile] = []
|
|
99
|
+
manifest: dict[str, Any] = {}
|
|
100
|
+
total = 0
|
|
101
|
+
with tarfile.open(archive_path, mode="r:gz") as archive:
|
|
102
|
+
for count, member in enumerate(archive):
|
|
103
|
+
if count > MAX_OUTPUT_FILES + 2 or not member.isreg():
|
|
104
|
+
raise CollectionError("The collected files are not valid.")
|
|
105
|
+
stream = archive.extractfile(member)
|
|
106
|
+
assert stream is not None
|
|
107
|
+
if member.name == MANIFEST_NAME:
|
|
108
|
+
manifest = json.loads(stream.read(1024 * 1024))
|
|
109
|
+
continue
|
|
110
|
+
if member.name == CHANGES_NAME and member.size <= MAX_CHANGES_BYTES:
|
|
111
|
+
stored_changes = store_stream(changes_path(run), stream, member.size)
|
|
112
|
+
continue
|
|
113
|
+
name = member.name.removeprefix(f"{OUTPUTS_DIR}/")
|
|
114
|
+
total += member.size
|
|
115
|
+
if (
|
|
116
|
+
name == member.name
|
|
117
|
+
or not safe_output_name(name)
|
|
118
|
+
or total > MAX_OUTPUT_BYTES
|
|
119
|
+
):
|
|
120
|
+
raise CollectionError("The collected files are not valid.")
|
|
121
|
+
stored = store_stream(saved_file_path(run, name), stream, member.size)
|
|
122
|
+
files.append(SavedFile(name=name, **stored))
|
|
123
|
+
changes = manifest.get("changes") or {}
|
|
124
|
+
return RunOutputs(
|
|
125
|
+
changes=Changes(
|
|
126
|
+
file_count=int(changes.get("file_count", 0)),
|
|
127
|
+
files=tuple(str(item) for item in changes.get("files", []))[
|
|
128
|
+
:MAX_LISTED_CHANGES
|
|
129
|
+
],
|
|
130
|
+
too_large=bool(changes.get("too_large")),
|
|
131
|
+
**stored_changes,
|
|
132
|
+
),
|
|
133
|
+
files=tuple(files),
|
|
134
|
+
skipped=tuple(
|
|
135
|
+
SkippedFile(
|
|
136
|
+
name=str(item.get("name", ""))[:255],
|
|
137
|
+
reason=str(item.get("reason", ""))[:200],
|
|
138
|
+
)
|
|
139
|
+
for item in manifest.get("skipped", [])[:MAX_OUTPUT_FILES]
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def store_stream(path: str, stream: IO[bytes], size: int) -> dict[str, Any]:
|
|
145
|
+
"""Save at a retry-stable path, and return the stored path, size and hash."""
|
|
146
|
+
reader = HashingReader(stream)
|
|
147
|
+
storage = artifact_storage()
|
|
148
|
+
storage.delete(path)
|
|
149
|
+
saved = storage.save(path, File(reader))
|
|
150
|
+
return {"path": saved, "size_bytes": size, "sha256": reader.digest.hexdigest()}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def run_output_paths(run: Run) -> list[str]:
|
|
154
|
+
"""Every stored file the sandbox left behind, for deletion with the run."""
|
|
155
|
+
return run.collected.stored_paths() if run.collected else []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def store_run_payload(run: Run, body: bytes) -> Payload:
|
|
159
|
+
"""Keep the request body that started a run, cut to TRIGGER_PAYLOAD_MAX_BYTES.
|
|
160
|
+
|
|
161
|
+
Returns what the run's input snapshot records about it.
|
|
162
|
+
"""
|
|
163
|
+
kept = body[: settings.TRIGGER_PAYLOAD_MAX_BYTES]
|
|
164
|
+
storage = artifact_storage()
|
|
165
|
+
path = payload_path(run)
|
|
166
|
+
storage.delete(path)
|
|
167
|
+
saved = storage.save(path, ContentFile(kept))
|
|
168
|
+
return Payload(
|
|
169
|
+
path=saved,
|
|
170
|
+
size_bytes=len(body),
|
|
171
|
+
kept_bytes=len(kept),
|
|
172
|
+
truncated=len(kept) < len(body),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def run_payload(run: Run) -> Payload | None:
|
|
177
|
+
return run.inputs.payload
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def store_run_input(run: Run, text: str) -> StoredOutput:
|
|
181
|
+
"""Store the rendered input, bounded like output, at a retry-stable path."""
|
|
182
|
+
raw = text.encode()
|
|
183
|
+
size = len(raw)
|
|
184
|
+
truncated = size > settings.RUN_OUTPUT_MAX_BYTES
|
|
185
|
+
if truncated:
|
|
186
|
+
raw = raw[: settings.RUN_OUTPUT_MAX_BYTES]
|
|
187
|
+
storage = artifact_storage()
|
|
188
|
+
path = input_path(run)
|
|
189
|
+
storage.delete(path)
|
|
190
|
+
saved = storage.save(path, ContentFile(raw))
|
|
191
|
+
return StoredOutput(
|
|
192
|
+
path=saved,
|
|
193
|
+
size_bytes=size,
|
|
194
|
+
sha256=hashlib.sha256(raw).hexdigest(),
|
|
195
|
+
truncated=truncated,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def store_run_text(run: Run, output: str) -> StoredOutput:
|
|
200
|
+
"""Store a model's final report without putting it in a temporary file."""
|
|
201
|
+
raw = output.encode()
|
|
202
|
+
size = len(raw)
|
|
203
|
+
truncated = size > settings.RUN_OUTPUT_MAX_BYTES
|
|
204
|
+
if truncated:
|
|
205
|
+
half = settings.RUN_OUTPUT_MAX_BYTES // 2
|
|
206
|
+
marker = TRUNCATION_MARKER.format(omitted=size - 2 * half).encode()
|
|
207
|
+
raw = raw[:half] + marker + raw[-half:]
|
|
208
|
+
storage = artifact_storage()
|
|
209
|
+
path = output_path(run)
|
|
210
|
+
storage.delete(path)
|
|
211
|
+
saved = storage.save(path, ContentFile(raw))
|
|
212
|
+
return StoredOutput(
|
|
213
|
+
path=saved,
|
|
214
|
+
size_bytes=size,
|
|
215
|
+
sha256=hashlib.sha256(raw).hexdigest(),
|
|
216
|
+
truncated=truncated,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def read_output_excerpt(path: str) -> str:
|
|
221
|
+
"""Return the end of a stored output, where the final result usually is."""
|
|
222
|
+
if not path:
|
|
223
|
+
return ""
|
|
224
|
+
limit = settings.RUN_OUTPUT_EXCERPT_BYTES
|
|
225
|
+
storage = artifact_storage()
|
|
226
|
+
with storage.open(path, "rb") as stream:
|
|
227
|
+
size = storage.size(path)
|
|
228
|
+
if size > limit:
|
|
229
|
+
stream.seek(size - limit)
|
|
230
|
+
return stream.read().decode(errors="replace")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def delete_output(path: str) -> None:
|
|
234
|
+
"""Delete a stored output. Runs call this through the post_delete signal."""
|
|
235
|
+
storage = artifact_storage()
|
|
236
|
+
storage.delete(path)
|
|
237
|
+
if isinstance(storage, FileSystemStorage):
|
|
238
|
+
remove_empty_parents(Path(storage.path(path)).parent, Path(storage.location))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def remove_empty_parents(directory: Path, root: Path) -> None:
|
|
242
|
+
"""Drop folders emptied by a delete, so day partitions do not pile up.
|
|
243
|
+
|
|
244
|
+
Object stores have no folders, so this only applies to the filesystem.
|
|
245
|
+
"""
|
|
246
|
+
root = root.resolve()
|
|
247
|
+
directory = directory.resolve()
|
|
248
|
+
while directory != root and directory.is_relative_to(root):
|
|
249
|
+
try:
|
|
250
|
+
directory.rmdir()
|
|
251
|
+
except OSError: # not empty, or already gone
|
|
252
|
+
return
|
|
253
|
+
directory = directory.parent
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def read_text(path: str, max_bytes: int) -> tuple[str, bool]:
|
|
257
|
+
"""The start of a stored file, and whether it was cut at max_bytes."""
|
|
258
|
+
storage = artifact_storage()
|
|
259
|
+
with storage.open(path, "rb") as stream:
|
|
260
|
+
raw = stream.read(max_bytes + 1)
|
|
261
|
+
return raw[:max_bytes].decode(errors="replace"), len(raw) > max_bytes
|