engineering-platform 2.2.0__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.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""CENTRAL-owned identity bindings for bounded Server-owned producers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from .platform_admin import require_installation_owner
|
|
12
|
+
|
|
13
|
+
DEPENDABOT = "DEPENDABOT"
|
|
14
|
+
GITHUB_REPOSITORY = "GITHUB_REPOSITORY"
|
|
15
|
+
_GITHUB = re.compile(r"^[a-z0-9][a-z0-9_.-]*/[a-z0-9][a-z0-9_.-]*$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProducerBindingError(ValueError):
|
|
19
|
+
"""Stable, fail-closed binding result."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, code: str) -> None:
|
|
22
|
+
super().__init__(code)
|
|
23
|
+
self.code = code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ExternalProducerBinding:
|
|
28
|
+
binding_id: str
|
|
29
|
+
project_id: str
|
|
30
|
+
repository_id: str
|
|
31
|
+
version: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _validate_binding_kind(producer_type: object, external_resource_type: object) -> None:
|
|
35
|
+
"""Keep the first registry deliberately bounded to approved producers."""
|
|
36
|
+
if producer_type != DEPENDABOT or external_resource_type != GITHUB_REPOSITORY:
|
|
37
|
+
raise ProducerBindingError("UNSUPPORTED_PRODUCER_BINDING")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_github_repository(value: object) -> str:
|
|
41
|
+
"""Normalize an external identity only; never inspect a local Git remote."""
|
|
42
|
+
if not isinstance(value, str):
|
|
43
|
+
raise ProducerBindingError("INVALID_EXTERNAL_IDENTITY")
|
|
44
|
+
normalized = value.strip().removesuffix("/").removesuffix(".git").casefold()
|
|
45
|
+
if normalized.startswith("https://github.com/"):
|
|
46
|
+
normalized = normalized.removeprefix("https://github.com/")
|
|
47
|
+
if normalized.startswith("git@github.com:"):
|
|
48
|
+
normalized = normalized.removeprefix("git@github.com:")
|
|
49
|
+
if not _GITHUB.fullmatch(normalized):
|
|
50
|
+
raise ProducerBindingError("INVALID_EXTERNAL_IDENTITY")
|
|
51
|
+
return normalized
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve(
|
|
55
|
+
connection: sqlite3.Connection,
|
|
56
|
+
*,
|
|
57
|
+
producer_type: str,
|
|
58
|
+
external_resource_type: str,
|
|
59
|
+
external_resource_identity: object,
|
|
60
|
+
) -> ExternalProducerBinding:
|
|
61
|
+
_validate_binding_kind(producer_type, external_resource_type)
|
|
62
|
+
identity = normalize_github_repository(external_resource_identity)
|
|
63
|
+
rows = connection.execute("""SELECT b.binding_id,b.project_id,b.repository_id,b.version,p.status,r.project_id
|
|
64
|
+
FROM ep_external_producer_bindings b JOIN ep_project_registrations p ON p.project_id=b.project_id
|
|
65
|
+
JOIN ep_repository_registrations r ON r.repository_id=b.repository_id
|
|
66
|
+
WHERE b.producer_type=? AND b.external_resource_type=? AND b.external_resource_identity=? AND b.status='ACTIVE'""", (producer_type, external_resource_type, identity)).fetchall()
|
|
67
|
+
if len(rows) != 1:
|
|
68
|
+
raise ProducerBindingError("BINDING_NOT_FOUND" if not rows else "BINDING_CONFLICT")
|
|
69
|
+
binding_id, project_id, repository_id, version, project_status, repository_project = rows[0]
|
|
70
|
+
if project_status != "ACTIVE":
|
|
71
|
+
raise ProducerBindingError("PROJECT_INACTIVE")
|
|
72
|
+
if project_id != repository_project:
|
|
73
|
+
raise ProducerBindingError("REPOSITORY_NOT_AUTHORIZED")
|
|
74
|
+
return ExternalProducerBinding(str(binding_id), str(project_id), str(repository_id), int(version))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def active_external_identities(
|
|
78
|
+
connection: sqlite3.Connection,
|
|
79
|
+
*,
|
|
80
|
+
producer_type: str,
|
|
81
|
+
external_resource_type: str,
|
|
82
|
+
) -> tuple[str, ...]:
|
|
83
|
+
"""Return only active observation targets for a bounded Server producer."""
|
|
84
|
+
_validate_binding_kind(producer_type, external_resource_type)
|
|
85
|
+
return tuple(
|
|
86
|
+
str(row[0])
|
|
87
|
+
for row in connection.execute(
|
|
88
|
+
"""SELECT external_resource_identity
|
|
89
|
+
FROM ep_external_producer_bindings
|
|
90
|
+
WHERE producer_type=? AND external_resource_type=? AND status='ACTIVE'
|
|
91
|
+
ORDER BY external_resource_identity""",
|
|
92
|
+
(producer_type, external_resource_type),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register(
|
|
98
|
+
connection: sqlite3.Connection,
|
|
99
|
+
*,
|
|
100
|
+
data_root: object,
|
|
101
|
+
producer_type: str,
|
|
102
|
+
external_resource_type: str,
|
|
103
|
+
external_resource_identity: object,
|
|
104
|
+
project_id: str,
|
|
105
|
+
repository_id: str,
|
|
106
|
+
reason: str,
|
|
107
|
+
) -> ExternalProducerBinding:
|
|
108
|
+
"""Register a binding through the installation-owner-only admin boundary."""
|
|
109
|
+
from pathlib import Path
|
|
110
|
+
actor = require_installation_owner(Path(data_root))
|
|
111
|
+
_validate_binding_kind(producer_type, external_resource_type)
|
|
112
|
+
if not isinstance(project_id, str) or not project_id or not isinstance(repository_id, str) or not repository_id:
|
|
113
|
+
raise ProducerBindingError("INVALID_BINDING")
|
|
114
|
+
if not isinstance(reason, str) or not reason.strip() or len(reason.strip()) > 512:
|
|
115
|
+
raise ProducerBindingError("INVALID_BINDING")
|
|
116
|
+
identity = normalize_github_repository(external_resource_identity)
|
|
117
|
+
target = connection.execute(
|
|
118
|
+
"""SELECT p.status,r.project_id
|
|
119
|
+
FROM ep_project_registrations AS p
|
|
120
|
+
LEFT JOIN ep_repository_registrations AS r ON r.repository_id=?
|
|
121
|
+
WHERE p.project_id=?""",
|
|
122
|
+
(repository_id, project_id),
|
|
123
|
+
).fetchone()
|
|
124
|
+
if target is None or target[0] != "ACTIVE" or target[1] != project_id:
|
|
125
|
+
raise ProducerBindingError("REPOSITORY_NOT_AUTHORIZED")
|
|
126
|
+
now, binding_id = datetime.now(timezone.utc).isoformat(), "binding-" + uuid4().hex
|
|
127
|
+
try:
|
|
128
|
+
connection.execute(
|
|
129
|
+
"""INSERT INTO ep_external_producer_bindings(
|
|
130
|
+
binding_id,producer_type,external_resource_type,external_resource_identity,
|
|
131
|
+
project_id,repository_id,status,version,created_at,created_by,updated_at,provenance
|
|
132
|
+
) VALUES(?,?,?,?,?,?, 'ACTIVE',1,?,?,?,?)""",
|
|
133
|
+
(
|
|
134
|
+
binding_id,
|
|
135
|
+
producer_type,
|
|
136
|
+
external_resource_type,
|
|
137
|
+
identity,
|
|
138
|
+
project_id,
|
|
139
|
+
repository_id,
|
|
140
|
+
now,
|
|
141
|
+
actor,
|
|
142
|
+
now,
|
|
143
|
+
json.dumps({"reason": reason.strip()}, sort_keys=True),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
except sqlite3.IntegrityError as error:
|
|
147
|
+
raise ProducerBindingError("BINDING_CONFLICT") from error
|
|
148
|
+
connection.execute(
|
|
149
|
+
"""INSERT INTO ep_external_producer_binding_audit(
|
|
150
|
+
binding_id,action,actor,reason,payload,recorded_at
|
|
151
|
+
) VALUES(?,?,?,?,?,?)""",
|
|
152
|
+
(
|
|
153
|
+
binding_id,
|
|
154
|
+
"REGISTER",
|
|
155
|
+
actor,
|
|
156
|
+
reason.strip(),
|
|
157
|
+
json.dumps(
|
|
158
|
+
{"identity": identity, "project_id": project_id, "repository_id": repository_id},
|
|
159
|
+
sort_keys=True,
|
|
160
|
+
),
|
|
161
|
+
now,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
return ExternalProducerBinding(binding_id, project_id, repository_id, 1)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def list_bindings(connection: sqlite3.Connection, *, data_root: object) -> list[dict[str, object]]:
|
|
168
|
+
"""Return bounded binding metadata to the installation-owner admin only."""
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
|
|
171
|
+
require_installation_owner(Path(data_root))
|
|
172
|
+
rows = connection.execute(
|
|
173
|
+
"""SELECT binding_id,producer_type,external_resource_type,external_resource_identity,
|
|
174
|
+
project_id,repository_id,status,version,created_at,updated_at
|
|
175
|
+
FROM ep_external_producer_bindings
|
|
176
|
+
ORDER BY producer_type,external_resource_type,external_resource_identity"""
|
|
177
|
+
).fetchall()
|
|
178
|
+
columns = (
|
|
179
|
+
"binding_id",
|
|
180
|
+
"producer_type",
|
|
181
|
+
"external_resource_type",
|
|
182
|
+
"external_resource_identity",
|
|
183
|
+
"project_id",
|
|
184
|
+
"repository_id",
|
|
185
|
+
"status",
|
|
186
|
+
"version",
|
|
187
|
+
"created_at",
|
|
188
|
+
"updated_at",
|
|
189
|
+
)
|
|
190
|
+
return [dict(zip(columns, row, strict=True)) for row in rows]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def deactivate(
|
|
194
|
+
connection: sqlite3.Connection,
|
|
195
|
+
*,
|
|
196
|
+
data_root: object,
|
|
197
|
+
binding_id: object,
|
|
198
|
+
reason: str,
|
|
199
|
+
) -> ExternalProducerBinding:
|
|
200
|
+
"""Deactivate exactly one active binding; historical provenance remains intact."""
|
|
201
|
+
from pathlib import Path
|
|
202
|
+
|
|
203
|
+
actor = require_installation_owner(Path(data_root))
|
|
204
|
+
if not isinstance(binding_id, str) or not binding_id or not isinstance(reason, str) or not reason.strip() or len(reason.strip()) > 512:
|
|
205
|
+
raise ProducerBindingError("INVALID_BINDING")
|
|
206
|
+
row = connection.execute(
|
|
207
|
+
"""SELECT binding_id,project_id,repository_id,version
|
|
208
|
+
FROM ep_external_producer_bindings
|
|
209
|
+
WHERE binding_id=? AND status='ACTIVE'""",
|
|
210
|
+
(binding_id,),
|
|
211
|
+
).fetchone()
|
|
212
|
+
if row is None:
|
|
213
|
+
raise ProducerBindingError("BINDING_NOT_ACTIVE")
|
|
214
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
215
|
+
next_version = int(row[3]) + 1
|
|
216
|
+
connection.execute(
|
|
217
|
+
"""UPDATE ep_external_producer_bindings
|
|
218
|
+
SET status='INACTIVE',version=?,updated_at=?
|
|
219
|
+
WHERE binding_id=? AND status='ACTIVE'""",
|
|
220
|
+
(next_version, now, binding_id),
|
|
221
|
+
)
|
|
222
|
+
connection.execute(
|
|
223
|
+
"""INSERT INTO ep_external_producer_binding_audit(
|
|
224
|
+
binding_id,action,actor,reason,payload,recorded_at
|
|
225
|
+
) VALUES(?,?,?,?,?,?)""",
|
|
226
|
+
(
|
|
227
|
+
binding_id,
|
|
228
|
+
"DEACTIVATE",
|
|
229
|
+
actor,
|
|
230
|
+
reason.strip(),
|
|
231
|
+
json.dumps({"previous_version": int(row[3])}, sort_keys=True),
|
|
232
|
+
now,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
return ExternalProducerBinding(str(row[0]), str(row[1]), str(row[2]), next_version)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Thin file ingress for canonical CENTRAL submissions.
|
|
2
|
+
|
|
3
|
+
This module owns only physical delivery acknowledgement. It does not open a
|
|
4
|
+
database, select work, or retain any run/lifecycle state. An explicit project
|
|
5
|
+
id is required in every file; a checkout name and current directory are never
|
|
6
|
+
consulted for authority.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from urllib.error import HTTPError, URLError
|
|
19
|
+
from urllib.request import Request, urlopen
|
|
20
|
+
from .submission_intake import SubmissionIntakeError, normalize_human_file
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MAX_FILE_BYTES = 131072
|
|
24
|
+
MAX_REASON_LENGTH = 160
|
|
25
|
+
HEARTBEAT_FILENAME = "file-inbox-heartbeat.json"
|
|
26
|
+
QUALIFICATION_FAULT_ENVIRONMENT = "EP_FILE_INBOX_QUALIFICATION_FAULT"
|
|
27
|
+
SUPPORTED_FILE_SUFFIXES = frozenset({".json", ".txt", ".md"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileInboxError(ValueError):
|
|
31
|
+
"""A bounded, terminal file-transport rejection."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Admission = Callable[[dict[str, object], str, str], dict[str, object]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _utcnow() -> str:
|
|
38
|
+
return datetime.now(timezone.utc).isoformat()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _layout(root: Path) -> dict[str, Path]:
|
|
42
|
+
folders = {name: root / name for name in ("incoming", "processing", "accepted", "quarantine")}
|
|
43
|
+
for folder in folders.values():
|
|
44
|
+
folder.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
45
|
+
return folders
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _reason(value: object) -> str:
|
|
49
|
+
rendered = str(value).replace("\n", " ").replace("\r", " ")
|
|
50
|
+
return rendered[:MAX_REASON_LENGTH] or "MALFORMED_FILE"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_envelope(path: Path) -> tuple[dict[str, object], bytes, str]:
|
|
54
|
+
raw = path.read_bytes()
|
|
55
|
+
if not 0 < len(raw) <= MAX_FILE_BYTES or b"\0" in raw:
|
|
56
|
+
raise FileInboxError("MALFORMED_FILE")
|
|
57
|
+
try:
|
|
58
|
+
envelope = json.loads(raw.decode("utf-8")) if path.suffix.lower() == ".json" else normalize_human_file(path)
|
|
59
|
+
except (UnicodeDecodeError, json.JSONDecodeError, SubmissionIntakeError) as error:
|
|
60
|
+
raise FileInboxError("MALFORMED_FILE") from error
|
|
61
|
+
if not isinstance(envelope, dict) or set(envelope) != {"project_id", "submission"}:
|
|
62
|
+
raise FileInboxError("MALFORMED_FILE")
|
|
63
|
+
if not isinstance(envelope["project_id"], str) or not envelope["project_id"]:
|
|
64
|
+
raise FileInboxError("MALFORMED_FILE")
|
|
65
|
+
if not isinstance(envelope["submission"], dict):
|
|
66
|
+
raise FileInboxError("MALFORMED_FILE")
|
|
67
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
68
|
+
return envelope, raw, digest
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _receipt_path(folder: Path, digest: str) -> Path:
|
|
72
|
+
return folder / f"{digest}.receipt.json"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _write_receipt(path: Path, payload: dict[str, object]) -> None:
|
|
76
|
+
temporary = path.with_suffix(".partial")
|
|
77
|
+
temporary.write_text(json.dumps(payload, sort_keys=True) + "\n", encoding="utf-8")
|
|
78
|
+
temporary.chmod(0o600)
|
|
79
|
+
os.replace(temporary, path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _move(source: Path, destination: Path) -> None:
|
|
83
|
+
if destination.exists():
|
|
84
|
+
source.unlink()
|
|
85
|
+
else:
|
|
86
|
+
os.replace(source, destination)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _submit(server: str, credential: str, envelope: dict[str, object], *, receipt_id: str, received_at: str) -> dict[str, object]:
|
|
90
|
+
project_id = str(envelope["project_id"])
|
|
91
|
+
submission = dict(envelope["submission"])
|
|
92
|
+
# This physical-file digest is the delivery key. It survives a restart
|
|
93
|
+
# between CENTRAL acceptance and archive acknowledgement.
|
|
94
|
+
submission["idempotency_key"] = receipt_id
|
|
95
|
+
submission["transport_receipt_id"] = receipt_id
|
|
96
|
+
submission["transport_received_at"] = received_at
|
|
97
|
+
request = Request(
|
|
98
|
+
server.rstrip("/") + f"/v1/projects/{project_id}/submissions",
|
|
99
|
+
data=json.dumps(submission, sort_keys=True).encode("utf-8"), method="POST",
|
|
100
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {credential}", "EP-Submission-Transport": "FILE_INBOX"},
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
with urlopen(request, timeout=15) as response: # nosec B310 -- configured CENTRAL endpoint
|
|
104
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
105
|
+
except HTTPError as error:
|
|
106
|
+
if error.code >= 500:
|
|
107
|
+
raise URLError("CENTRAL_UNAVAILABLE") from error
|
|
108
|
+
try:
|
|
109
|
+
detail = json.loads(error.read().decode("utf-8")).get("error", "CENTRAL_REJECTED")
|
|
110
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
111
|
+
detail = "CENTRAL_REJECTED"
|
|
112
|
+
raise FileInboxError(_reason(detail)) from error
|
|
113
|
+
except (URLError, TimeoutError) as error:
|
|
114
|
+
raise URLError("CENTRAL_UNAVAILABLE") from error
|
|
115
|
+
if not isinstance(result, dict) or not isinstance(result.get("submission_id"), str):
|
|
116
|
+
raise URLError("CENTRAL_UNAVAILABLE")
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def process_once(
|
|
121
|
+
root: Path, *, server: str | None = None, credential: str | None = None,
|
|
122
|
+
admission: Admission | None = None,
|
|
123
|
+
) -> dict[str, int]:
|
|
124
|
+
"""Deliver every pending file once; unavailable CENTRAL leaves it retryable.
|
|
125
|
+
|
|
126
|
+
The installed Server child supplies ``admission``. It is an in-process
|
|
127
|
+
call to the same canonical application service used after HTTP/CLI caller
|
|
128
|
+
authentication; File Inbox never gets a project bearer credential.
|
|
129
|
+
``server``/``credential`` remain private test seams for the transport's
|
|
130
|
+
public HTTP equivalence tests and are not an installed executable surface.
|
|
131
|
+
"""
|
|
132
|
+
folders = _layout(root)
|
|
133
|
+
counts = {"accepted": 0, "quarantined": 0, "retryable": 0}
|
|
134
|
+
for source in sorted(path for path in folders["incoming"].iterdir() if path.suffix.lower() in SUPPORTED_FILE_SUFFIXES):
|
|
135
|
+
claimed = folders["processing"] / source.name
|
|
136
|
+
_move(source, claimed)
|
|
137
|
+
for claimed in sorted(path for path in folders["processing"].iterdir() if path.suffix.lower() in SUPPORTED_FILE_SUFFIXES):
|
|
138
|
+
try:
|
|
139
|
+
envelope, _raw, digest = _read_envelope(claimed)
|
|
140
|
+
# Installed qualification only: terminate the real Server process
|
|
141
|
+
# at a named transport boundary. Normal runtime never sets this.
|
|
142
|
+
if os.environ.get(QUALIFICATION_FAULT_ENVIRONMENT) == "AFTER_CLAIM_BEFORE_SUBMIT":
|
|
143
|
+
os._exit(86) # nosec B605 -- deliberate crash-canary boundary
|
|
144
|
+
receipt_id, received_at = f"file:{digest}", _utcnow()
|
|
145
|
+
if admission is not None:
|
|
146
|
+
receipt = admission(envelope, receipt_id, received_at)
|
|
147
|
+
elif server is not None and credential is not None:
|
|
148
|
+
receipt = _submit(server, credential, envelope, receipt_id=receipt_id, received_at=received_at)
|
|
149
|
+
else:
|
|
150
|
+
raise FileInboxError("FILE_INBOX_AUTH_UNAVAILABLE")
|
|
151
|
+
if os.environ.get(QUALIFICATION_FAULT_ENVIRONMENT) == "AFTER_CENTRAL_ACCEPT_BEFORE_ARCHIVE":
|
|
152
|
+
os._exit(87) # nosec B605 -- deliberate crash-canary boundary
|
|
153
|
+
submission = envelope["submission"]
|
|
154
|
+
constraints = submission.get("constraints") if isinstance(submission, dict) else {}
|
|
155
|
+
normalized = json.dumps(envelope, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
156
|
+
_write_receipt(_receipt_path(folders["accepted"], digest), {
|
|
157
|
+
"transport": "FILE_INBOX", "receipt_id": receipt_id, "received_at": received_at,
|
|
158
|
+
"source_digest": digest, "project_id": envelope["project_id"],
|
|
159
|
+
"repository_id": submission.get("repository_id") if isinstance(submission, dict) else None,
|
|
160
|
+
"requested_mode": constraints.get("mode") if isinstance(constraints, dict) else None,
|
|
161
|
+
"normalization_method": constraints.get("normalization") if isinstance(constraints, dict) else "STRUCTURED",
|
|
162
|
+
"normalization_version": constraints.get("normalization") if isinstance(constraints, dict) else "STRUCTURED_V1",
|
|
163
|
+
"normalized_submission_digest": hashlib.sha256(normalized).hexdigest(),
|
|
164
|
+
"submission_id": receipt["submission_id"], "duplicate": bool(receipt.get("duplicate")),
|
|
165
|
+
})
|
|
166
|
+
_move(claimed, folders["accepted"] / f"{digest}.json")
|
|
167
|
+
counts["accepted"] += 1
|
|
168
|
+
except FileInboxError as error:
|
|
169
|
+
# A terminal parser/admission error is transport acknowledgement,
|
|
170
|
+
# not a CENTRAL lifecycle mutation.
|
|
171
|
+
reason = _reason(error)
|
|
172
|
+
digest = hashlib.sha256(claimed.name.encode("utf-8")).hexdigest()
|
|
173
|
+
_write_receipt(_receipt_path(folders["quarantine"], digest), {"transport": "FILE_INBOX", "reason": reason})
|
|
174
|
+
_move(claimed, folders["quarantine"] / f"{digest}.json")
|
|
175
|
+
counts["quarantined"] += 1
|
|
176
|
+
except URLError:
|
|
177
|
+
counts["retryable"] += 1
|
|
178
|
+
return counts
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def heartbeat_path(root: Path) -> Path:
|
|
182
|
+
"""Return the installation-owned liveness record for this adapter.
|
|
183
|
+
|
|
184
|
+
This is deliberately a small, secret-free transport observation. It is
|
|
185
|
+
neither a queue nor a lifecycle store; CENTRAL remains the sole authority
|
|
186
|
+
for accepted submissions and dispatch state.
|
|
187
|
+
"""
|
|
188
|
+
return root / HEARTBEAT_FILENAME
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def read_heartbeat(root: Path) -> dict[str, object] | None:
|
|
192
|
+
try:
|
|
193
|
+
payload = json.loads(heartbeat_path(root).read_text(encoding="utf-8"))
|
|
194
|
+
except (OSError, json.JSONDecodeError):
|
|
195
|
+
return None
|
|
196
|
+
return payload if isinstance(payload, dict) else None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class FileInboxService:
|
|
200
|
+
"""Installed Server-composed File Inbox adapter with a bounded heartbeat."""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self, root: Path, *, admission: Admission | None = None,
|
|
204
|
+
server: str | None = None, credential: str | None = None, interval_seconds: float = 2.0,
|
|
205
|
+
) -> None:
|
|
206
|
+
self.root, self.admission, self.server, self.credential = root, admission, server, credential
|
|
207
|
+
self.interval_seconds = interval_seconds
|
|
208
|
+
self._stop = threading.Event()
|
|
209
|
+
self._thread: threading.Thread | None = None
|
|
210
|
+
self._counts = {"accepted": 0, "quarantined": 0, "retryable": 0}
|
|
211
|
+
self._recent_error: str | None = None
|
|
212
|
+
|
|
213
|
+
def _write_heartbeat(self) -> None:
|
|
214
|
+
folders = _layout(self.root)
|
|
215
|
+
ready = self.admission is not None
|
|
216
|
+
payload = {
|
|
217
|
+
# A live thread without a submission credential cannot admit a
|
|
218
|
+
# file. Process liveness must never be projected as readiness.
|
|
219
|
+
"state": "READY" if ready else "RUNNING_NOT_READY",
|
|
220
|
+
"readiness": "SUBMISSION_CAPABLE" if ready else "AUTHENTICATION_UNAVAILABLE",
|
|
221
|
+
"updated_at": _utcnow(),
|
|
222
|
+
"watched_location": str(self.root),
|
|
223
|
+
"delivery_retry": "PENDING" if self._counts["retryable"] else "NONE",
|
|
224
|
+
"quarantine_count": len(list(folders["quarantine"].glob("*.json"))),
|
|
225
|
+
"recent_error": self._recent_error or (None if ready else "FILE_INBOX_AUTH_UNAVAILABLE"),
|
|
226
|
+
}
|
|
227
|
+
_write_receipt(heartbeat_path(self.root), payload)
|
|
228
|
+
|
|
229
|
+
def _run(self) -> None:
|
|
230
|
+
while not self._stop.is_set():
|
|
231
|
+
try:
|
|
232
|
+
if self.admission is not None:
|
|
233
|
+
self._counts = process_once(self.root, admission=self.admission)
|
|
234
|
+
self._recent_error = None
|
|
235
|
+
self._write_heartbeat()
|
|
236
|
+
except (OSError, ValueError, URLError) as error:
|
|
237
|
+
self._recent_error = _reason(error)
|
|
238
|
+
self._write_heartbeat()
|
|
239
|
+
self._stop.wait(self.interval_seconds)
|
|
240
|
+
|
|
241
|
+
def start(self) -> None:
|
|
242
|
+
_layout(self.root)
|
|
243
|
+
self._thread = threading.Thread(target=self._run, name="engineering-platform-file-inbox", daemon=True)
|
|
244
|
+
self._thread.start()
|
|
245
|
+
|
|
246
|
+
def stop(self) -> None:
|
|
247
|
+
self._stop.set()
|
|
248
|
+
if self._thread is not None:
|
|
249
|
+
self._thread.join(timeout=self.interval_seconds + 1)
|