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.
Potentially problematic release.
This version of engineering-platform might be problematic. Click here for more details.
- 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,385 @@
|
|
|
1
|
+
"""Idempotent repository bootstrap and workspace provisioning API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import sqlite3
|
|
9
|
+
|
|
10
|
+
from .platform_api import PlatformConfiguration, PlatformConfigurationError, shared_workspace_store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WORKSPACE_DIRECTORY = ".engineering"
|
|
14
|
+
LEGACY_WORKSPACE_DIRECTORY = ".djconnect"
|
|
15
|
+
_AUTO_IDENTIFIER_TABLES = frozenset({
|
|
16
|
+
"engineering_artifacts",
|
|
17
|
+
"engineering_component_logs",
|
|
18
|
+
"execution_lease_events",
|
|
19
|
+
"execution_lifecycle_events",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkspaceMigrationBlockedError(RuntimeError):
|
|
24
|
+
"""A one-time workspace migration found a component that is still live."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, component: str | None = None) -> None:
|
|
27
|
+
self.component = component or "another component"
|
|
28
|
+
super().__init__("Engineering workspace migration requires running components to stop first.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _worktree_roots(root: Path) -> tuple[Path, ...]:
|
|
32
|
+
"""Return accessible worktrees that share ``root``'s Git common directory."""
|
|
33
|
+
root = root.resolve()
|
|
34
|
+
shared = shared_workspace_store(root)
|
|
35
|
+
if shared == root / WORKSPACE_DIRECTORY:
|
|
36
|
+
return (root,)
|
|
37
|
+
common = shared.parent
|
|
38
|
+
roots = {common.parent.resolve(), root}
|
|
39
|
+
worktrees = common / "worktrees"
|
|
40
|
+
if worktrees.is_dir():
|
|
41
|
+
for entry in worktrees.iterdir():
|
|
42
|
+
marker = entry / "gitdir"
|
|
43
|
+
try:
|
|
44
|
+
worktree_git_marker = Path(marker.read_text(encoding="utf-8").strip())
|
|
45
|
+
except OSError:
|
|
46
|
+
continue
|
|
47
|
+
candidate = worktree_git_marker.parent.resolve()
|
|
48
|
+
if (candidate / ".git").exists():
|
|
49
|
+
roots.add(candidate)
|
|
50
|
+
return tuple(sorted(roots, key=lambda item: str(item)))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _history_count(workspace: Path) -> int:
|
|
54
|
+
"""Use the immutable history index only to choose an initial store seed."""
|
|
55
|
+
database = workspace / "engineering.db"
|
|
56
|
+
if not database.is_file():
|
|
57
|
+
return 0
|
|
58
|
+
try:
|
|
59
|
+
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
|
|
60
|
+
return int(connection.execute("SELECT COUNT(*) FROM prompt_execution_history").fetchone()[0])
|
|
61
|
+
except (sqlite3.DatabaseError, OSError):
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _database_tables(connection: sqlite3.Connection, schema: str) -> tuple[str, ...]:
|
|
66
|
+
return tuple(
|
|
67
|
+
row[0]
|
|
68
|
+
for row in connection.execute(
|
|
69
|
+
f"SELECT name FROM {schema}.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _merge_databases(source: Path, destination: Path) -> None:
|
|
75
|
+
"""Merge independently written local evidence without discarding run history.
|
|
76
|
+
|
|
77
|
+
Engineering records are append-only by identity. SQLite row ids are local
|
|
78
|
+
implementation details for log/event tables, so those are regenerated at
|
|
79
|
+
the destination while their durable content is retained. Mutable status
|
|
80
|
+
projections take the newest timestamp. Any incompatible schema fails
|
|
81
|
+
closed before either source evidence or the shared store is removed.
|
|
82
|
+
"""
|
|
83
|
+
with sqlite3.connect(destination) as connection:
|
|
84
|
+
connection.execute("PRAGMA foreign_keys=OFF")
|
|
85
|
+
connection.execute("ATTACH DATABASE ? AS legacy", (str(source),))
|
|
86
|
+
in_transaction = False
|
|
87
|
+
try:
|
|
88
|
+
current_tables = _database_tables(connection, "main")
|
|
89
|
+
legacy_tables = _database_tables(connection, "legacy")
|
|
90
|
+
if current_tables != legacy_tables:
|
|
91
|
+
raise RuntimeError("Engineering workspace migration found incompatible database schemas.")
|
|
92
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
93
|
+
in_transaction = True
|
|
94
|
+
for table in current_tables:
|
|
95
|
+
columns = [row[1] for row in connection.execute(f"PRAGMA main.table_info({table})")]
|
|
96
|
+
if table in _AUTO_IDENTIFIER_TABLES:
|
|
97
|
+
columns = [column for column in columns if column != "id"]
|
|
98
|
+
if not columns:
|
|
99
|
+
continue
|
|
100
|
+
column_list = ",".join(columns)
|
|
101
|
+
if table in {"engineering_status", "engineering_transactions"}:
|
|
102
|
+
key = "name" if table == "engineering_status" else "run_id"
|
|
103
|
+
for row in connection.execute(f"SELECT {column_list} FROM legacy.{table}"):
|
|
104
|
+
values = dict(zip(columns, row, strict=True))
|
|
105
|
+
current = connection.execute(
|
|
106
|
+
f"SELECT updated_at FROM main.{table} WHERE {key}=?", (values[key],)
|
|
107
|
+
).fetchone()
|
|
108
|
+
if current is None:
|
|
109
|
+
placeholders = ",".join("?" for _ in columns)
|
|
110
|
+
connection.execute(
|
|
111
|
+
f"INSERT INTO main.{table}({column_list}) VALUES({placeholders})",
|
|
112
|
+
tuple(values[column] for column in columns),
|
|
113
|
+
)
|
|
114
|
+
elif values["updated_at"] > current[0]:
|
|
115
|
+
assignments = ["payload=?", "updated_at=?"]
|
|
116
|
+
parameters: list[object] = [values["payload"], values["updated_at"]]
|
|
117
|
+
if table == "engineering_transactions":
|
|
118
|
+
assignments.insert(1, "phase=?")
|
|
119
|
+
parameters.insert(1, values["phase"])
|
|
120
|
+
parameters.append(values[key])
|
|
121
|
+
connection.execute(
|
|
122
|
+
f"UPDATE main.{table} SET {','.join(assignments)} WHERE {key}=?", parameters
|
|
123
|
+
)
|
|
124
|
+
elif table == "daily_execution_statistics":
|
|
125
|
+
# This is a rebuildable read model. The next telemetry
|
|
126
|
+
# update writes its fresh aggregate; never overwrite a
|
|
127
|
+
# newer shared projection during migration.
|
|
128
|
+
connection.execute(
|
|
129
|
+
f"INSERT OR IGNORE INTO main.{table}({column_list}) SELECT {column_list} FROM legacy.{table}"
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
connection.execute(
|
|
133
|
+
f"INSERT OR IGNORE INTO main.{table}({column_list}) SELECT {column_list} FROM legacy.{table}"
|
|
134
|
+
)
|
|
135
|
+
violations = connection.execute("PRAGMA foreign_key_check").fetchall()
|
|
136
|
+
if violations:
|
|
137
|
+
raise RuntimeError("Engineering workspace migration would violate datastore integrity.")
|
|
138
|
+
connection.execute("COMMIT")
|
|
139
|
+
in_transaction = False
|
|
140
|
+
except Exception:
|
|
141
|
+
if in_transaction:
|
|
142
|
+
connection.execute("ROLLBACK")
|
|
143
|
+
raise
|
|
144
|
+
finally:
|
|
145
|
+
connection.execute("DETACH DATABASE legacy")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _merge_workspace(source: Path, destination: Path) -> None:
|
|
149
|
+
"""Merge one worktree's local evidence into the shared workspace."""
|
|
150
|
+
for child in tuple(source.iterdir()):
|
|
151
|
+
target = destination / child.name
|
|
152
|
+
if child.name == "engineering.db" and target.is_file():
|
|
153
|
+
_merge_databases(child, target)
|
|
154
|
+
child.unlink()
|
|
155
|
+
elif not target.exists():
|
|
156
|
+
shutil.move(str(child), str(target))
|
|
157
|
+
elif child.is_dir() and target.is_dir():
|
|
158
|
+
_merge_workspace(child, target)
|
|
159
|
+
child.rmdir()
|
|
160
|
+
elif child.is_file() and target.is_file() and child.read_bytes() == target.read_bytes():
|
|
161
|
+
child.unlink()
|
|
162
|
+
else:
|
|
163
|
+
raise RuntimeError(f"Engineering workspace migration conflict: {child.name}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _link_workspace(worktree: Path, shared: Path) -> None:
|
|
167
|
+
"""Expose the shared private store at the established worktree-local path."""
|
|
168
|
+
local = worktree / WORKSPACE_DIRECTORY
|
|
169
|
+
if local.is_symlink():
|
|
170
|
+
if local.resolve() != shared.resolve():
|
|
171
|
+
raise RuntimeError("Engineering workspace points to an unexpected shared store.")
|
|
172
|
+
return
|
|
173
|
+
if local.exists():
|
|
174
|
+
raise RuntimeError("Engineering workspace migration did not consume a local store.")
|
|
175
|
+
os.symlink(shared, local, target_is_directory=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _discard_inactive_component_locks(workspace: Path) -> None:
|
|
179
|
+
"""Discard only stale process locks before relocating their directory.
|
|
180
|
+
|
|
181
|
+
A flock is meaningful only in the filesystem currently held by its owner;
|
|
182
|
+
copying that file into a shared workspace would create a false lock. A
|
|
183
|
+
live owner therefore blocks the migration until the normal component
|
|
184
|
+
restart has stopped it. Stale lock files are explicitly non-evidence and
|
|
185
|
+
may be recreated by their owning component after the migration.
|
|
186
|
+
"""
|
|
187
|
+
locks = workspace / "locks"
|
|
188
|
+
if not locks.exists():
|
|
189
|
+
return
|
|
190
|
+
if locks.is_symlink() or not locks.is_dir():
|
|
191
|
+
raise RuntimeError("Engineering workspace migration found an invalid component-lock directory.")
|
|
192
|
+
for path in locks.glob("*.lock"):
|
|
193
|
+
try:
|
|
194
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
195
|
+
process_id = payload.get("pid") if isinstance(payload, dict) else None
|
|
196
|
+
if isinstance(process_id, int) and process_id > 0:
|
|
197
|
+
os.kill(process_id, 0)
|
|
198
|
+
component = payload.get("component") if isinstance(payload, dict) else None
|
|
199
|
+
raise WorkspaceMigrationBlockedError(component if isinstance(component, str) else None)
|
|
200
|
+
except ProcessLookupError:
|
|
201
|
+
continue
|
|
202
|
+
except PermissionError as error:
|
|
203
|
+
raise RuntimeError("Engineering workspace migration cannot verify a component lock owner.") from error
|
|
204
|
+
except (OSError, json.JSONDecodeError):
|
|
205
|
+
continue
|
|
206
|
+
shutil.rmtree(locks)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def migrate_worktree_workspace(root: Path) -> Path:
|
|
210
|
+
"""Create one durable Engineering store for every worktree of a repository."""
|
|
211
|
+
root = root.resolve()
|
|
212
|
+
shared = shared_workspace_store(root)
|
|
213
|
+
if shared == root / WORKSPACE_DIRECTORY:
|
|
214
|
+
return shared
|
|
215
|
+
candidates = [worktree / WORKSPACE_DIRECTORY for worktree in _worktree_roots(root)]
|
|
216
|
+
local_directories = [path for path in candidates if path.exists() and not path.is_symlink()]
|
|
217
|
+
if not shared.exists() and local_directories:
|
|
218
|
+
seed = max(local_directories, key=lambda path: (_history_count(path), str(path)))
|
|
219
|
+
shared.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
220
|
+
shutil.move(str(seed), str(shared))
|
|
221
|
+
shared.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
222
|
+
if local_directories:
|
|
223
|
+
_discard_inactive_component_locks(shared)
|
|
224
|
+
for local in local_directories:
|
|
225
|
+
if local.exists():
|
|
226
|
+
_discard_inactive_component_locks(local)
|
|
227
|
+
for local in local_directories:
|
|
228
|
+
if not local.exists():
|
|
229
|
+
continue
|
|
230
|
+
_merge_workspace(local, shared)
|
|
231
|
+
local.rmdir()
|
|
232
|
+
for worktree in _worktree_roots(root):
|
|
233
|
+
_link_workspace(worktree, shared)
|
|
234
|
+
return shared.resolve()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _validate_legacy_merge(source: Path, destination: Path) -> None:
|
|
238
|
+
"""Fail closed before moving evidence into an occupied canonical workspace."""
|
|
239
|
+
if source.is_symlink() or destination.is_symlink():
|
|
240
|
+
raise RuntimeError("Engineering workspace migration refuses symbolic links.")
|
|
241
|
+
if source.is_dir() != destination.is_dir():
|
|
242
|
+
raise RuntimeError(f"Engineering workspace migration conflict: {source.name}")
|
|
243
|
+
if source.is_dir():
|
|
244
|
+
for child in source.iterdir():
|
|
245
|
+
target = destination / child.name
|
|
246
|
+
if target.exists():
|
|
247
|
+
_validate_legacy_merge(child, target)
|
|
248
|
+
return
|
|
249
|
+
if source.read_bytes() != destination.read_bytes():
|
|
250
|
+
raise RuntimeError(f"Engineering workspace migration conflict: {source.name}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _merge_legacy_workspace(source: Path, destination: Path) -> None:
|
|
254
|
+
"""Move prevalidated legacy evidence, dropping only byte-identical duplicates."""
|
|
255
|
+
for child in source.iterdir():
|
|
256
|
+
target = destination / child.name
|
|
257
|
+
if not target.exists():
|
|
258
|
+
shutil.move(str(child), str(target))
|
|
259
|
+
elif child.is_dir():
|
|
260
|
+
_merge_legacy_workspace(child, target)
|
|
261
|
+
child.rmdir()
|
|
262
|
+
else:
|
|
263
|
+
child.unlink()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _move_legacy_logs(source: Path, workspace: Path) -> None:
|
|
267
|
+
"""Preserve a conflicting historic log tail outside the live log files."""
|
|
268
|
+
destination = workspace / "logs" / "legacy"
|
|
269
|
+
if destination.exists():
|
|
270
|
+
_validate_legacy_merge(source, destination)
|
|
271
|
+
_merge_legacy_workspace(source, destination)
|
|
272
|
+
source.rmdir()
|
|
273
|
+
else:
|
|
274
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
275
|
+
shutil.move(str(source), str(destination))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _archive_legacy_evidence(source: Path, workspace: Path) -> None:
|
|
279
|
+
"""Keep a conflicting historic evidence category without replacing live data."""
|
|
280
|
+
destination = workspace / "legacy" / source.name
|
|
281
|
+
if destination.exists():
|
|
282
|
+
_validate_legacy_merge(source, destination)
|
|
283
|
+
_merge_legacy_workspace(source, destination)
|
|
284
|
+
source.rmdir()
|
|
285
|
+
else:
|
|
286
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
287
|
+
shutil.move(str(source), str(destination))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def migrate_legacy_workspace(root: Path) -> Path:
|
|
291
|
+
"""Move `.djconnect` evidence to the sole canonical `.engineering` location."""
|
|
292
|
+
root = root.resolve()
|
|
293
|
+
workspace = migrate_worktree_workspace(root)
|
|
294
|
+
legacy = root / LEGACY_WORKSPACE_DIRECTORY
|
|
295
|
+
if not legacy.exists():
|
|
296
|
+
return workspace
|
|
297
|
+
if legacy.is_symlink() or not legacy.is_dir():
|
|
298
|
+
raise RuntimeError("Engineering workspace migration requires a local directory.")
|
|
299
|
+
workspace.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
300
|
+
archived_categories = {"logs", "qualification"}
|
|
301
|
+
for source in legacy.iterdir():
|
|
302
|
+
if source.name in archived_categories and (workspace / source.name).exists():
|
|
303
|
+
continue
|
|
304
|
+
target = workspace / source.name
|
|
305
|
+
if target.exists():
|
|
306
|
+
_validate_legacy_merge(source, target)
|
|
307
|
+
legacy_logs = legacy / "logs"
|
|
308
|
+
if legacy_logs.exists() and (workspace / "logs").exists():
|
|
309
|
+
_move_legacy_logs(legacy_logs, workspace)
|
|
310
|
+
legacy_qualification = legacy / "qualification"
|
|
311
|
+
if legacy_qualification.exists() and (workspace / "qualification").exists():
|
|
312
|
+
_archive_legacy_evidence(legacy_qualification, workspace)
|
|
313
|
+
_merge_legacy_workspace(legacy, workspace)
|
|
314
|
+
legacy.rmdir()
|
|
315
|
+
return workspace
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def provision_workspace(root: Path) -> dict[str, Path]:
|
|
319
|
+
"""Provision only platform-owned local directories; safe to repeat."""
|
|
320
|
+
workspace = migrate_legacy_workspace(root)
|
|
321
|
+
return _provision_workspace_paths(root, workspace)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def runtime_workspace(root: Path) -> Path:
|
|
325
|
+
"""Return the already-valid shared store without starting a migration.
|
|
326
|
+
|
|
327
|
+
A long-running dashboard is allowed to coexist with a watcher or runner.
|
|
328
|
+
When this worktree already points to the canonical shared store, a normal
|
|
329
|
+
component start must not inspect or merge dormant legacy stores belonging
|
|
330
|
+
to another worktree. Such a merge remains an explicit, fail-closed
|
|
331
|
+
migration through :func:`migrate_legacy_workspace`.
|
|
332
|
+
"""
|
|
333
|
+
root = root.resolve()
|
|
334
|
+
shared = shared_workspace_store(root)
|
|
335
|
+
local = root / WORKSPACE_DIRECTORY
|
|
336
|
+
legacy = root / LEGACY_WORKSPACE_DIRECTORY
|
|
337
|
+
if (
|
|
338
|
+
shared != local
|
|
339
|
+
and local.is_symlink()
|
|
340
|
+
and local.resolve() == shared.resolve()
|
|
341
|
+
and shared.is_dir()
|
|
342
|
+
and not legacy.exists()
|
|
343
|
+
):
|
|
344
|
+
return shared.resolve()
|
|
345
|
+
return migrate_legacy_workspace(root)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _provision_workspace_paths(root: Path, workspace: Path) -> dict[str, Path]:
|
|
349
|
+
"""Create the repeatable directories for an already-selected workspace."""
|
|
350
|
+
PlatformConfiguration.load(root)
|
|
351
|
+
paths = {"workspace": workspace, "reports": workspace / "reports", "status": workspace / "status", "diagnostics": workspace / "logs", "inbox_processing": workspace / "inbox-processing"}
|
|
352
|
+
# A CENTRAL lifecycle must not even create the historical checkpoint
|
|
353
|
+
# directory. Checkout directories retained here are physical temporary
|
|
354
|
+
# execution concerns, not operational truth.
|
|
355
|
+
if not os.environ.get("EP_CENTRAL_OPERATIONAL_DATABASE"):
|
|
356
|
+
paths["runs"] = workspace / "engineering-runs"
|
|
357
|
+
for path in paths.values():
|
|
358
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
359
|
+
return paths
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def provision_runtime_workspace(root: Path) -> dict[str, Path]:
|
|
363
|
+
"""Provision a watcher, runner, or dashboard without implicit migration."""
|
|
364
|
+
return _provision_workspace_paths(root, runtime_workspace(root))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def validate_repository(root: Path) -> PlatformConfiguration:
|
|
368
|
+
"""Fail closed unless this repository is an explicit platform consumer."""
|
|
369
|
+
if not (root / "BOOTSTRAP.md").is_file() or not (root / ".git").exists():
|
|
370
|
+
raise PlatformConfigurationError("Repository bootstrap compatibility failed.")
|
|
371
|
+
return PlatformConfiguration.load(root)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def render_template(destination: Path, replacements: dict[str, str]) -> Path:
|
|
375
|
+
"""Create a deterministic config template without overwriting consumer data."""
|
|
376
|
+
if destination.exists():
|
|
377
|
+
return destination
|
|
378
|
+
template = Path(__file__).with_name("templates") / "workspace-config.json"
|
|
379
|
+
content = template.read_text(encoding="utf-8")
|
|
380
|
+
for key, value in sorted(replacements.items()):
|
|
381
|
+
content = content.replace(key, value)
|
|
382
|
+
json.loads(content)
|
|
383
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
384
|
+
destination.write_text(content + "\n", encoding="utf-8")
|
|
385
|
+
return destination
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The sole installed-Server inventory of supported Platform Components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class PlatformComponent:
|
|
10
|
+
"""Stable product, health and logging identity for one Server component."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
name_key: str
|
|
14
|
+
kind: str
|
|
15
|
+
group: str
|
|
16
|
+
active_status: str
|
|
17
|
+
inactive_status: str
|
|
18
|
+
detail_code: str
|
|
19
|
+
startup_event: str
|
|
20
|
+
restart_supported: bool = False
|
|
21
|
+
critical: bool = False
|
|
22
|
+
lifecycle_label: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
PLATFORM_COMPONENTS = (
|
|
26
|
+
PlatformComponent("ep_server", "component.ep_server", "DAEMON", "platform", "EP_SERVER_ACTIVE", "EP_SERVER_UNAVAILABLE", "EP_SERVER_ENDPOINT", "ep_server_started", False, True),
|
|
27
|
+
PlatformComponent("platform_database", "component.platform_database", "STORAGE", "platform", "PLATFORM_DATABASE_HEALTHY", "PLATFORM_DATABASE_UNAVAILABLE", "PLATFORM_DATABASE_STORAGE", "central_log_store_ready", False, True),
|
|
28
|
+
PlatformComponent("lifecycle_worker", "component.lifecycle_worker", "IN_PROCESS_COMPONENT", "platform", "LIFECYCLE_WORKER_ACTIVE", "LIFECYCLE_WORKER_UNAVAILABLE", "LIFECYCLE_WORKER_SERVER_HOSTED", "lifecycle_worker_started", False, True),
|
|
29
|
+
PlatformComponent("operations_console", "component.operations_console", "UI_SERVICE", "access", "OPERATIONS_CONSOLE_AVAILABLE", "OPERATIONS_CONSOLE_UNAVAILABLE", "OPERATIONS_CONSOLE_SERVER_NATIVE", "operations_console_available"),
|
|
30
|
+
PlatformComponent(
|
|
31
|
+
"dashboard_relay", "component.dashboard_relay", "UI_SERVICE", "access",
|
|
32
|
+
"DASHBOARD_RELAY_ACTIVE", "DASHBOARD_RELAY_UNAVAILABLE",
|
|
33
|
+
"DASHBOARD_RELAY_SERVER_NATIVE", "dashboard_relay_available",
|
|
34
|
+
restart_supported=True,
|
|
35
|
+
lifecycle_label="com.engineeringplatform.dashboard-relay",
|
|
36
|
+
),
|
|
37
|
+
PlatformComponent("http_ingress", "transport.http", "TRANSPORT", "ingress", "HTTP_INGRESS_HEALTHY", "HTTP_INGRESS_DOWN", "CENTRAL_LISTENER_ENDPOINT", "http_ingress_available", False, True),
|
|
38
|
+
PlatformComponent("cli_ingress", "transport.cli", "TRANSPORT", "ingress", "CLI_INGRESS_AVAILABLE", "CLI_INGRESS_DEGRADED", "CANONICAL_SUBMISSION_COMPATIBILITY", "cli_ingress_available", False, True),
|
|
39
|
+
PlatformComponent("file_inbox_ingress", "transport.file", "TRANSPORT", "ingress", "FILE_INGRESS_RUNNING", "FILE_INGRESS_STOPPED", "FILE_INBOX_HEARTBEAT", "file_inbox_service_started"),
|
|
40
|
+
PlatformComponent("dependabot_producer", "component.dependabot_producer", "TRANSPORT", "ingress", "DEPENDABOT_READY", "DEPENDABOT_DEGRADED", "DEPENDABOT_HEARTBEAT", "dependabot_producer_started"),
|
|
41
|
+
)
|
|
42
|
+
PLATFORM_COMPONENT_BY_ID = {component.id: component for component in PLATFORM_COMPONENTS}
|
|
43
|
+
PLATFORM_COMPONENT_IDS = frozenset(PLATFORM_COMPONENT_BY_ID)
|
|
44
|
+
# The installed product has exactly these submission boundaries. The Local
|
|
45
|
+
# Consumer API is historical compatibility code, not a fourth ingress.
|
|
46
|
+
SUPPORTED_SUBMISSION_INGRESSES = ("HTTP_JSON", "INSTALLED_CLI", "FILE_INBOX")
|
|
47
|
+
SUPPORTED_SUBMISSION_INGRESS_COUNT = len(SUPPORTED_SUBMISSION_INGRESSES)
|
|
48
|
+
# Route consumers import this value instead of duplicating a literal inventory.
|
|
49
|
+
PLATFORM_COMPONENT_ROUTE_PATTERN = "(?:" + "|".join(component.id for component in PLATFORM_COMPONENTS) + ")"
|
|
50
|
+
|
|
51
|
+
# These are retired *input* identifiers, not components and never aliases for
|
|
52
|
+
# a supported component. Keeping the bounded denial list beside the canonical
|
|
53
|
+
# model prevents a compatibility parser or route from quietly turning one back
|
|
54
|
+
# into lifecycle, logging or configuration authority.
|
|
55
|
+
RETIRED_COMPONENT_ALIASES = frozenset({
|
|
56
|
+
"dashboard",
|
|
57
|
+
"dashboard_service",
|
|
58
|
+
"dashboard_watcher",
|
|
59
|
+
"finder",
|
|
60
|
+
"inbox",
|
|
61
|
+
"inbox_service",
|
|
62
|
+
"inbox_watcher",
|
|
63
|
+
"watcher",
|
|
64
|
+
})
|
|
65
|
+
RETIRED_COMPONENT_ALIAS_ROUTE_PATTERN = "(?:" + "|".join(sorted(RETIRED_COMPONENT_ALIASES)) + ")"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Deterministic Engineering Platform manifest and compatibility validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .storage import ENGINEERING_STORAGE_SCHEMA_VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
14
|
+
CONTRACT = re.compile(r"^(\d{4})\.(0[1-9]|1[0-2])$")
|
|
15
|
+
CURRENT_PLATFORM_VERSION = "2.2.0"
|
|
16
|
+
MANIFEST_FIELDS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"platform_version",
|
|
19
|
+
"runner_version",
|
|
20
|
+
"bootstrap_contract",
|
|
21
|
+
"checkpoint_format",
|
|
22
|
+
"memory_format",
|
|
23
|
+
"report_format",
|
|
24
|
+
"minimum_codex_cli",
|
|
25
|
+
"watcher_version",
|
|
26
|
+
"inbox_protocol",
|
|
27
|
+
"dashboard_version",
|
|
28
|
+
"handoff_protocol",
|
|
29
|
+
"status_model",
|
|
30
|
+
"storage_schema",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EngineeringPlatformCompatibilityError(ValueError):
|
|
36
|
+
"""Raised when a repository's declared engineering contract is unsupported."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _semver(value: str, field: str) -> tuple[int, int, int]:
|
|
40
|
+
match = SEMVER.fullmatch(value) if isinstance(value, str) else None
|
|
41
|
+
if not match:
|
|
42
|
+
raise EngineeringPlatformCompatibilityError(
|
|
43
|
+
f"Engineering Platform manifest field {field} must use MAJOR.MINOR.PATCH."
|
|
44
|
+
)
|
|
45
|
+
return tuple(int(part) for part in match.groups())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _contract(value: str, field: str) -> tuple[int, int]:
|
|
49
|
+
match = CONTRACT.fullmatch(value) if isinstance(value, str) else None
|
|
50
|
+
if not match:
|
|
51
|
+
raise EngineeringPlatformCompatibilityError(
|
|
52
|
+
f"Engineering Platform manifest field {field} must use YYYY.MM."
|
|
53
|
+
)
|
|
54
|
+
return tuple(int(part) for part in match.groups())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class EngineeringPlatformManifest:
|
|
59
|
+
platform_version: str
|
|
60
|
+
runner_version: str
|
|
61
|
+
bootstrap_contract: str
|
|
62
|
+
checkpoint_format: int
|
|
63
|
+
memory_format: int
|
|
64
|
+
report_format: int
|
|
65
|
+
minimum_codex_cli: str
|
|
66
|
+
watcher_version: str
|
|
67
|
+
inbox_protocol: int
|
|
68
|
+
dashboard_version: str
|
|
69
|
+
handoff_protocol: int
|
|
70
|
+
status_model: int
|
|
71
|
+
storage_schema: int
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def load(cls, path: Path) -> "EngineeringPlatformManifest":
|
|
75
|
+
try:
|
|
76
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
77
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
78
|
+
raise EngineeringPlatformCompatibilityError(
|
|
79
|
+
"Engineering Platform manifest cannot be read."
|
|
80
|
+
) from error
|
|
81
|
+
if not isinstance(raw, dict) or set(raw) != MANIFEST_FIELDS:
|
|
82
|
+
raise EngineeringPlatformCompatibilityError(
|
|
83
|
+
"Engineering Platform manifest fields are incompatible."
|
|
84
|
+
)
|
|
85
|
+
manifest = cls(**raw)
|
|
86
|
+
_semver(manifest.platform_version, "platform_version")
|
|
87
|
+
_semver(manifest.runner_version, "runner_version")
|
|
88
|
+
_semver(manifest.minimum_codex_cli, "minimum_codex_cli")
|
|
89
|
+
_semver(manifest.watcher_version, "watcher_version")
|
|
90
|
+
_semver(manifest.dashboard_version, "dashboard_version")
|
|
91
|
+
_contract(manifest.bootstrap_contract, "bootstrap_contract")
|
|
92
|
+
for field in (
|
|
93
|
+
"checkpoint_format",
|
|
94
|
+
"memory_format",
|
|
95
|
+
"report_format",
|
|
96
|
+
"inbox_protocol",
|
|
97
|
+
"handoff_protocol",
|
|
98
|
+
"status_model",
|
|
99
|
+
"storage_schema",
|
|
100
|
+
):
|
|
101
|
+
if not isinstance(getattr(manifest, field), int) or getattr(manifest, field) < 1:
|
|
102
|
+
raise EngineeringPlatformCompatibilityError(
|
|
103
|
+
f"Engineering Platform manifest field {field} must be a positive integer."
|
|
104
|
+
)
|
|
105
|
+
return manifest
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class RunnerCompatibility:
|
|
110
|
+
platform_version: str = CURRENT_PLATFORM_VERSION
|
|
111
|
+
runner_version: str = CURRENT_PLATFORM_VERSION
|
|
112
|
+
bootstrap_contract: str = "2026.12"
|
|
113
|
+
checkpoint_formats: frozenset[int] = frozenset({1})
|
|
114
|
+
memory_formats: frozenset[int] = frozenset({1, 2})
|
|
115
|
+
report_formats: frozenset[int] = frozenset({1, 2})
|
|
116
|
+
# New runners retain compatibility with prior local stores while accepting
|
|
117
|
+
# the current telemetry-capable schema.
|
|
118
|
+
storage_schemas: frozenset[int] = frozenset(
|
|
119
|
+
range(1, ENGINEERING_STORAGE_SCHEMA_VERSION + 1)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_compatibility(
|
|
124
|
+
manifest: EngineeringPlatformManifest, runner: RunnerCompatibility, detected_codex_cli: str
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Fail closed unless this runner explicitly supports every repository contract."""
|
|
127
|
+
required_platform = _semver(manifest.platform_version, "platform_version")
|
|
128
|
+
actual_platform = _semver(runner.platform_version, "runner platform_version")
|
|
129
|
+
required_runner = _semver(manifest.runner_version, "runner_version")
|
|
130
|
+
actual_runner = _semver(runner.runner_version, "runner_version")
|
|
131
|
+
if required_platform[0] != actual_platform[0]:
|
|
132
|
+
raise EngineeringPlatformCompatibilityError(
|
|
133
|
+
f"Engineering Platform mismatch\nRepository requires: {manifest.platform_version}\nRunner: {runner.platform_version}\nBLOCKED\nEngineering Platform upgrade required."
|
|
134
|
+
)
|
|
135
|
+
if actual_runner < required_runner:
|
|
136
|
+
raise EngineeringPlatformCompatibilityError(
|
|
137
|
+
f"Runner version mismatch\nRepository requires: {manifest.runner_version}\nRunner: {runner.runner_version}\nBLOCKED\nRunner upgrade required."
|
|
138
|
+
)
|
|
139
|
+
required_contract = _contract(manifest.bootstrap_contract, "bootstrap_contract")
|
|
140
|
+
actual_contract = _contract(runner.bootstrap_contract, "runner bootstrap_contract")
|
|
141
|
+
if actual_contract < required_contract:
|
|
142
|
+
raise EngineeringPlatformCompatibilityError(
|
|
143
|
+
f"Bootstrap contract mismatch\nRepository requires: {manifest.bootstrap_contract}\nRunner: {runner.bootstrap_contract}\nBLOCKED\nEngineering Platform upgrade required."
|
|
144
|
+
)
|
|
145
|
+
for label, required, supported in (
|
|
146
|
+
("Checkpoint format", manifest.checkpoint_format, runner.checkpoint_formats),
|
|
147
|
+
("Engineering Memory format", manifest.memory_format, runner.memory_formats),
|
|
148
|
+
("Report format", manifest.report_format, runner.report_formats),
|
|
149
|
+
("Engineering storage schema", manifest.storage_schema, runner.storage_schemas),
|
|
150
|
+
):
|
|
151
|
+
if required not in supported:
|
|
152
|
+
detected = ", ".join(str(value) for value in sorted(supported)) or "none"
|
|
153
|
+
raise EngineeringPlatformCompatibilityError(
|
|
154
|
+
f"{label} mismatch\nRepository requires: {required}\nRunner supports: {detected}\nBLOCKED\nEngineering Platform upgrade required."
|
|
155
|
+
)
|
|
156
|
+
required_cli = _semver(manifest.minimum_codex_cli, "minimum_codex_cli")
|
|
157
|
+
detected_cli = _semver(detected_codex_cli, "detected Codex CLI version")
|
|
158
|
+
if detected_cli < required_cli:
|
|
159
|
+
raise EngineeringPlatformCompatibilityError(
|
|
160
|
+
f"Codex CLI version mismatch\nRepository requires: {manifest.minimum_codex_cli}\nDetected: {detected_codex_cli}\nBLOCKED\nCodex CLI upgrade required."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def detected_codex_cli_version(output: str) -> str:
|
|
165
|
+
"""Extract a stable semantic version from the local CLI version output."""
|
|
166
|
+
match = re.search(r"\b(\d+\.\d+\.\d+)\b", output)
|
|
167
|
+
if not match:
|
|
168
|
+
raise EngineeringPlatformCompatibilityError(
|
|
169
|
+
"Detected Codex CLI version is invalid. Run `codex --version` and install a supported release."
|
|
170
|
+
)
|
|
171
|
+
return match.group(1)
|