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,199 @@
|
|
|
1
|
+
"""Server-owned, containment-safe Host Admin observations and mutation gate.
|
|
2
|
+
|
|
3
|
+
Host Admin never discovers a target from a request, project, checkout, CWD,
|
|
4
|
+
remote, browser, or Finder. Deployments provide an explicit registry; this
|
|
5
|
+
module resolves opaque identifiers from that registry only.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
from . import managed_codex_runtime
|
|
17
|
+
from .component_logging import component_logger, log_event
|
|
18
|
+
|
|
19
|
+
_TARGET_ID = re.compile(r"[a-z][a-z0-9-]{0,63}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HostAdminTargetError(ValueError):
|
|
23
|
+
"""A requested Host Admin target is absent, stale, or unsafe."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolved(path: Path) -> Path:
|
|
27
|
+
try:
|
|
28
|
+
return path.resolve(strict=True)
|
|
29
|
+
except OSError as error:
|
|
30
|
+
raise HostAdminTargetError("HOST_ADMIN_TARGET_STALE") from error
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _contained(path: Path, root: Path) -> bool:
|
|
34
|
+
try:
|
|
35
|
+
path.relative_to(root)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class HostAdminTarget:
|
|
43
|
+
"""A deployment-approved Git/worktree boundary, never request-derived."""
|
|
44
|
+
|
|
45
|
+
target_id: str
|
|
46
|
+
containment_root: Path
|
|
47
|
+
primary_worktree: Path
|
|
48
|
+
worktrees: tuple[tuple[str, Path], ...] = ()
|
|
49
|
+
|
|
50
|
+
def validated(self) -> "HostAdminTarget":
|
|
51
|
+
if not _TARGET_ID.fullmatch(self.target_id):
|
|
52
|
+
raise HostAdminTargetError("HOST_ADMIN_TARGET_INVALID")
|
|
53
|
+
root, primary = _resolved(self.containment_root), _resolved(self.primary_worktree)
|
|
54
|
+
if not root.is_dir() or not primary.is_dir() or not _contained(primary, root):
|
|
55
|
+
raise HostAdminTargetError("HOST_ADMIN_TARGET_OUTSIDE_CONTAINMENT")
|
|
56
|
+
seen: set[str] = set(); approved: list[tuple[str, Path]] = []
|
|
57
|
+
for worktree_id, worktree in self.worktrees:
|
|
58
|
+
if not _TARGET_ID.fullmatch(worktree_id) or worktree_id in seen:
|
|
59
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_TARGET_INVALID")
|
|
60
|
+
candidate = _resolved(worktree)
|
|
61
|
+
if not candidate.is_dir() or not _contained(candidate, root):
|
|
62
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_ESCAPE")
|
|
63
|
+
seen.add(worktree_id); approved.append((worktree_id, candidate))
|
|
64
|
+
return HostAdminTarget(self.target_id, root, primary, tuple(approved))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HostAdminTargetRegistry:
|
|
68
|
+
"""The only source of mutable Host Admin target identity."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, targets: Iterable[HostAdminTarget] = ()) -> None:
|
|
71
|
+
values = [target.validated() for target in targets]
|
|
72
|
+
if len({target.target_id for target in values}) != len(values):
|
|
73
|
+
raise HostAdminTargetError("HOST_ADMIN_TARGET_DUPLICATE")
|
|
74
|
+
self._targets = {target.target_id: target for target in values}
|
|
75
|
+
|
|
76
|
+
def target(self, target_id: object) -> HostAdminTarget:
|
|
77
|
+
if not isinstance(target_id, str) or target_id not in self._targets:
|
|
78
|
+
raise HostAdminTargetError("HOST_ADMIN_TARGET_UNKNOWN")
|
|
79
|
+
return self._targets[target_id].validated()
|
|
80
|
+
|
|
81
|
+
def worktree(self, target_id: object, worktree_id: object) -> Path:
|
|
82
|
+
target = self.target(target_id)
|
|
83
|
+
if not isinstance(worktree_id, str):
|
|
84
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_TARGET_UNKNOWN")
|
|
85
|
+
for registered_id, path in target.worktrees:
|
|
86
|
+
if registered_id == worktree_id:
|
|
87
|
+
return path
|
|
88
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_TARGET_UNKNOWN")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def installation_root(data_root: Path) -> Path:
|
|
92
|
+
return data_root.resolve()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def registry(_data_root: Path) -> HostAdminTargetRegistry:
|
|
96
|
+
"""Fail closed until deployment provisions explicit targets outside HTTP."""
|
|
97
|
+
return HostAdminTargetRegistry()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _audit(data_root: Path, event: str, *, target_id: object, outcome: str) -> None:
|
|
101
|
+
log_event(component_logger(data_root, "ep_server"), 20, event,
|
|
102
|
+
diagnostic=f"target_id={target_id!r}; outcome={outcome}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def worktree_inventory(targets: HostAdminTargetRegistry, target_id: object) -> dict[str, object]:
|
|
106
|
+
"""Return registered worktrees that Git currently reports, in containment."""
|
|
107
|
+
target = targets.target(target_id)
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(("git", "-C", str(target.primary_worktree), "worktree", "list", "--porcelain"),
|
|
110
|
+
check=False, capture_output=True, text=True)
|
|
111
|
+
except OSError as error:
|
|
112
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_INVENTORY_UNAVAILABLE") from error
|
|
113
|
+
if result.returncode != 0:
|
|
114
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_INVENTORY_UNAVAILABLE")
|
|
115
|
+
observed = {line.split(" ", 1)[1] for line in result.stdout.splitlines() if line.startswith("worktree ")}
|
|
116
|
+
rows = []
|
|
117
|
+
for worktree_id, path in target.worktrees:
|
|
118
|
+
current = _resolved(path)
|
|
119
|
+
rows.append({"worktree_id": worktree_id, "registered": True,
|
|
120
|
+
"present": str(current) in observed,
|
|
121
|
+
"primary": current == target.primary_worktree, "path": str(current)})
|
|
122
|
+
return {"target_id": target.target_id, "worktrees": rows}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def diagnose_git_lock(targets: HostAdminTargetRegistry, target_id: object) -> dict[str, object]:
|
|
126
|
+
"""Diagnose exactly the primary index lock; never accept a lock pathname."""
|
|
127
|
+
target = targets.target(target_id)
|
|
128
|
+
lock = target.primary_worktree / ".git" / "index.lock"
|
|
129
|
+
if not lock.parent.is_dir() or not _contained(_resolved(lock.parent), target.containment_root):
|
|
130
|
+
raise HostAdminTargetError("HOST_ADMIN_GIT_LOCK_AMBIGUOUS")
|
|
131
|
+
if lock.is_symlink():
|
|
132
|
+
raise HostAdminTargetError("HOST_ADMIN_GIT_LOCK_ESCAPE")
|
|
133
|
+
active_owner = False
|
|
134
|
+
if lock.exists():
|
|
135
|
+
try:
|
|
136
|
+
probe = subprocess.run(("lsof", "--", str(lock)), check=False,
|
|
137
|
+
capture_output=True, text=True)
|
|
138
|
+
active_owner = probe.returncode == 0 and bool(probe.stdout.strip())
|
|
139
|
+
except OSError:
|
|
140
|
+
# No owner probe never authorizes a mutation; repair remains
|
|
141
|
+
# removed, while this bounded diagnostic remains available.
|
|
142
|
+
active_owner = False
|
|
143
|
+
return {"target_id": target.target_id, "lock": str(lock), "exists": lock.exists(),
|
|
144
|
+
"active_owner": active_owner, "repairable": False}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def remove_worktree(data_root: Path, targets: HostAdminTargetRegistry,
|
|
148
|
+
target_id: object, worktree_id: object) -> dict[str, object]:
|
|
149
|
+
"""Gate legacy removal after registration/inventory checks; deletion is removed."""
|
|
150
|
+
try:
|
|
151
|
+
target = targets.target(target_id); worktree = targets.worktree(target_id, worktree_id)
|
|
152
|
+
inventory = worktree_inventory(targets, target_id)
|
|
153
|
+
row = next((item for item in inventory["worktrees"] if item["worktree_id"] == worktree_id), None)
|
|
154
|
+
if not isinstance(row, dict) or not row["present"]:
|
|
155
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_TARGET_STALE")
|
|
156
|
+
if worktree == target.primary_worktree or row["primary"]:
|
|
157
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_PRIMARY_PROTECTED")
|
|
158
|
+
# A dirty worktree is never an administrative cleanup candidate. A
|
|
159
|
+
# Git index lock is the bounded active-owner signal available without
|
|
160
|
+
# process discovery; ambiguity fails closed.
|
|
161
|
+
status = subprocess.run(("git", "-C", str(worktree), "status", "--porcelain"),
|
|
162
|
+
check=False, capture_output=True, text=True)
|
|
163
|
+
if status.returncode != 0:
|
|
164
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_STATUS_UNAVAILABLE")
|
|
165
|
+
if status.stdout.strip():
|
|
166
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_DIRTY")
|
|
167
|
+
if (worktree / ".git" / "index.lock").exists():
|
|
168
|
+
raise HostAdminTargetError("HOST_ADMIN_WORKTREE_ACTIVE")
|
|
169
|
+
_audit(data_root, "host_admin_worktree_removal_refused", target_id=target_id, outcome="UNSUPPORTED_REMOVED")
|
|
170
|
+
return {"outcome": "UNSUPPORTED_REMOVED", "worktree_id": worktree_id}
|
|
171
|
+
except HostAdminTargetError as error:
|
|
172
|
+
_audit(data_root, "host_admin_worktree_removal_rejected", target_id=target_id, outcome=str(error))
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def repair_git_lock(data_root: Path, targets: HostAdminTargetRegistry, target_id: object) -> dict[str, object]:
|
|
177
|
+
"""Gate legacy repair after exact diagnosis; arbitrary lock deletion is removed."""
|
|
178
|
+
try:
|
|
179
|
+
diagnosis = diagnose_git_lock(targets, target_id)
|
|
180
|
+
if not diagnosis["exists"]:
|
|
181
|
+
raise HostAdminTargetError("HOST_ADMIN_GIT_LOCK_ABSENT")
|
|
182
|
+
if diagnosis["active_owner"]:
|
|
183
|
+
raise HostAdminTargetError("HOST_ADMIN_GIT_LOCK_ACTIVE")
|
|
184
|
+
_audit(data_root, "host_admin_git_lock_repair_refused", target_id=target_id, outcome="UNSUPPORTED_REMOVED")
|
|
185
|
+
return {"outcome": "UNSUPPORTED_REMOVED", "target_id": target_id}
|
|
186
|
+
except HostAdminTargetError as error:
|
|
187
|
+
_audit(data_root, "host_admin_git_lock_repair_rejected", target_id=target_id, outcome=str(error))
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def diagnostics(data_root: Path) -> dict[str, object]:
|
|
192
|
+
root = installation_root(data_root); usage = shutil.disk_usage(root)
|
|
193
|
+
runtime = managed_codex_runtime.inspect(root)
|
|
194
|
+
state = runtime.get("state") if isinstance(runtime, dict) else "UNKNOWN"
|
|
195
|
+
return {"scope": "HOST_ADMIN", "root_kind": "EP_SERVER_INSTALLATION",
|
|
196
|
+
"disk": {"total_bytes": usage.total, "used_bytes": usage.used, "free_bytes": usage.free},
|
|
197
|
+
"managed_codex_runtime": {"state": state}, "registered_targets": 0,
|
|
198
|
+
"mutations_supported": False, "project_authority": False,
|
|
199
|
+
"execution_authority": False, "queue_authority": False}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Fail-closed Level 1 health checks for the local Engineering Execution Host.
|
|
2
|
+
|
|
3
|
+
This module intentionally validates only the host's own configuration, runtime
|
|
4
|
+
and evidence services. It never inspects a target repository or prompt.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import sqlite3
|
|
17
|
+
import tempfile
|
|
18
|
+
from time import monotonic
|
|
19
|
+
|
|
20
|
+
from .component_logging import component_logger
|
|
21
|
+
from .platform_api import PlatformConfiguration, PlatformConfigurationError
|
|
22
|
+
from .platform_version import EngineeringPlatformManifest
|
|
23
|
+
from .storage import open_storage
|
|
24
|
+
from .drift_diagnostics import evidence_for_checks, guidance, persist as persist_drift_evidence
|
|
25
|
+
from .providers import LocalProcessProvider
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEFAULT_MINIMUM_FREE_BYTES = 1_073_741_824
|
|
29
|
+
MINIMUM_FREE_BYTES_ENVIRONMENT = "ENGINEERING_PLATFORM_PREFLIGHT_MIN_FREE_BYTES"
|
|
30
|
+
TELEMETRY_ENABLED_ENVIRONMENT = "ENGINEERING_PLATFORM_TELEMETRY_PERSISTENCE"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class HostPreflightCheck:
|
|
35
|
+
identifier: str
|
|
36
|
+
outcome: str
|
|
37
|
+
reason: str
|
|
38
|
+
recovery: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class HostPreflightResult:
|
|
43
|
+
outcome: str
|
|
44
|
+
execution_host: str
|
|
45
|
+
version: str
|
|
46
|
+
bootstrap_contract: str
|
|
47
|
+
runtime_path: str | None
|
|
48
|
+
runtime_version: str | None
|
|
49
|
+
timestamp: str
|
|
50
|
+
duration_ms: int
|
|
51
|
+
checks: tuple[HostPreflightCheck, ...]
|
|
52
|
+
drift_evidence: tuple[dict[str, str], ...] = ()
|
|
53
|
+
resume_guidance: dict[str, object] | None = None
|
|
54
|
+
|
|
55
|
+
def payload(self, run_id: str | None = None) -> dict[str, object]:
|
|
56
|
+
result = asdict(self)
|
|
57
|
+
result["checks"] = [asdict(check) for check in self.checks]
|
|
58
|
+
result["run_id"] = run_id
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _check(identifier: str, passed: bool, reason: str, recovery: str) -> HostPreflightCheck:
|
|
63
|
+
return HostPreflightCheck(identifier, "PASS" if passed else "FAIL", reason, recovery)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_RUNTIME_VERSION = re.compile(r"\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _runtime_version(output: str) -> str | None:
|
|
70
|
+
"""Keep only the compact version token reported by the managed runtime."""
|
|
71
|
+
match = _RUNTIME_VERSION.search(output.strip())
|
|
72
|
+
return match.group(0) if match else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _minimum_free_bytes() -> int:
|
|
76
|
+
value = os.environ.get(MINIMUM_FREE_BYTES_ENVIRONMENT, str(DEFAULT_MINIMUM_FREE_BYTES))
|
|
77
|
+
try:
|
|
78
|
+
parsed = int(value)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return -1
|
|
81
|
+
return parsed if parsed >= 0 else -1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _telemetry_enabled() -> bool:
|
|
85
|
+
return os.environ.get(TELEMETRY_ENABLED_ENVIRONMENT, "true").strip().casefold() not in {"0", "false", "no"}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _writable(path: Path) -> bool:
|
|
89
|
+
if not path.is_dir():
|
|
90
|
+
return False
|
|
91
|
+
try:
|
|
92
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".preflight-", dir=path)
|
|
93
|
+
os.close(descriptor)
|
|
94
|
+
Path(temporary).unlink(missing_ok=True)
|
|
95
|
+
return True
|
|
96
|
+
except OSError:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _persist(root: Path, result: HostPreflightResult, run_id: str | None) -> None:
|
|
101
|
+
directory = root / ".engineering" / "status"
|
|
102
|
+
if not directory.is_dir() or not _writable(directory):
|
|
103
|
+
return
|
|
104
|
+
payload = json.dumps(result.payload(run_id), separators=(",", ":"), sort_keys=True) + "\n"
|
|
105
|
+
try:
|
|
106
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".host-preflight-", dir=directory)
|
|
107
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
108
|
+
handle.write(payload)
|
|
109
|
+
handle.flush()
|
|
110
|
+
os.fsync(handle.fileno())
|
|
111
|
+
os.replace(temporary, directory / "host_preflight.json")
|
|
112
|
+
except OSError:
|
|
113
|
+
try:
|
|
114
|
+
Path(temporary).unlink(missing_ok=True)
|
|
115
|
+
except UnboundLocalError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def execute(root: Path, *, run_id: str | None = None) -> HostPreflightResult:
|
|
120
|
+
"""Run Level 1 checks and persist bounded evidence without claiming work."""
|
|
121
|
+
started = monotonic()
|
|
122
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
123
|
+
checks: list[HostPreflightCheck] = []
|
|
124
|
+
configuration = None
|
|
125
|
+
try:
|
|
126
|
+
configuration = PlatformConfiguration.load(root)
|
|
127
|
+
checks.append(_check("configuration", True, "Required host configuration is readable.", "No action required."))
|
|
128
|
+
except PlatformConfigurationError:
|
|
129
|
+
checks.append(_check("configuration", False, "Required host configuration is unavailable.", "Restore a valid Engineering Platform configuration."))
|
|
130
|
+
|
|
131
|
+
manifest: EngineeringPlatformManifest | None = None
|
|
132
|
+
try:
|
|
133
|
+
manifest = EngineeringPlatformManifest.load(Path(__file__).with_name("ENGINEERING_PLATFORM_VERSION.json"))
|
|
134
|
+
checks.append(_check("host_identity", True, "Execution Host identity, version and bootstrap contract are available.", "No action required."))
|
|
135
|
+
except Exception:
|
|
136
|
+
checks.append(_check("host_identity", False, "Execution Host identity or bootstrap contract is unavailable.", "Restore the Engineering Platform version manifest."))
|
|
137
|
+
|
|
138
|
+
directories = (
|
|
139
|
+
("status", configuration.resolver(root).resolve_status_store() if configuration else None),
|
|
140
|
+
("reports", configuration.resolver(root).resolve_report_store() if configuration else None),
|
|
141
|
+
("logs", configuration.resolver(root).resolve_log_store() if configuration else None),
|
|
142
|
+
("inbox-processing", configuration.resolver(root).resolve_workspace_store() / "inbox-processing" if configuration else None),
|
|
143
|
+
)
|
|
144
|
+
for name, path in directories:
|
|
145
|
+
path = path if path is not None else root / ".engineering" / name
|
|
146
|
+
checks.append(_check(f"directory_{name}", path.is_dir(), f"Runtime directory {name} is available." if path.is_dir() else f"Runtime directory {name} is missing.", f"Create and secure .engineering/{name} before accepting work."))
|
|
147
|
+
if path.is_dir():
|
|
148
|
+
writable = _writable(path)
|
|
149
|
+
checks.append(_check(f"writable_{name}", writable, f"Runtime directory {name} is writable." if writable else f"Runtime directory {name} is not writable.", f"Restore write access to .engineering/{name}."))
|
|
150
|
+
|
|
151
|
+
threshold = _minimum_free_bytes()
|
|
152
|
+
if threshold < 0:
|
|
153
|
+
checks.append(_check("disk_space", False, "Configured free-disk threshold is invalid.", f"Set {MINIMUM_FREE_BYTES_ENVIRONMENT} to a non-negative byte value."))
|
|
154
|
+
else:
|
|
155
|
+
free = shutil.disk_usage(root).free
|
|
156
|
+
checks.append(_check("disk_space", free >= threshold, "Sufficient free disk space is available." if free >= threshold else "Free disk space is below the configured host threshold.", "Free disk space or lower the configured host preflight threshold."))
|
|
157
|
+
|
|
158
|
+
runtime_path: str | None = None
|
|
159
|
+
runtime_version: str | None = None
|
|
160
|
+
try:
|
|
161
|
+
executable = str(configuration.resolver(root).resolve_runtime()) if configuration is not None else None
|
|
162
|
+
runtime_path = executable
|
|
163
|
+
except PlatformConfigurationError:
|
|
164
|
+
executable = None
|
|
165
|
+
checks.append(_check("runtime_executable", bool(executable), "Configured runtime executable is available." if executable else "Configured runtime executable is unavailable.", "Install or expose the Codex CLI on the Execution Host PATH."))
|
|
166
|
+
if executable:
|
|
167
|
+
try:
|
|
168
|
+
invoked = LocalProcessProvider().execute(root, (executable, "--version"))
|
|
169
|
+
available = invoked.returncode == 0
|
|
170
|
+
if available:
|
|
171
|
+
runtime_version = _runtime_version(invoked.stdout)
|
|
172
|
+
except OSError:
|
|
173
|
+
available = False
|
|
174
|
+
checks.append(_check("runtime_invocation", available, "Configured runtime executable is invokable." if available else "Configured runtime executable cannot be invoked.", "Repair the Codex CLI installation before accepting work."))
|
|
175
|
+
|
|
176
|
+
if _telemetry_enabled():
|
|
177
|
+
try:
|
|
178
|
+
connection = open_storage(root)
|
|
179
|
+
try:
|
|
180
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
181
|
+
connection.rollback()
|
|
182
|
+
finally:
|
|
183
|
+
connection.close()
|
|
184
|
+
checks.append(_check("telemetry_storage", True, "Telemetry SQLite storage is accessible and writable.", "No action required."))
|
|
185
|
+
except (OSError, sqlite3.DatabaseError, RuntimeError):
|
|
186
|
+
checks.append(_check("telemetry_storage", False, "Telemetry SQLite storage is unavailable.", "Restore local SQLite evidence storage before accepting work."))
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Preflight has no legacy watcher logger. File Inbox is the canonical
|
|
190
|
+
# transport identity when the Server publishes a CENTRAL log binding.
|
|
191
|
+
component_logger(root, "file_inbox_ingress")
|
|
192
|
+
checks.append(_check("structured_logging", True, "Structured logging initializes successfully.", "No action required."))
|
|
193
|
+
except Exception:
|
|
194
|
+
checks.append(_check("structured_logging", False, "Structured logging cannot initialize.", "Restore the local logging destination before accepting work."))
|
|
195
|
+
|
|
196
|
+
outcome = (
|
|
197
|
+
"FAIL"
|
|
198
|
+
if any(check.outcome == "FAIL" for check in checks)
|
|
199
|
+
else "WARNING"
|
|
200
|
+
if any(check.outcome == "WARNING" for check in checks)
|
|
201
|
+
else "PASS"
|
|
202
|
+
)
|
|
203
|
+
drift_evidence = persist_drift_evidence(root, evidence_for_checks(
|
|
204
|
+
checks, stage="Execution Host Preflight", repository=str(root.resolve())
|
|
205
|
+
))
|
|
206
|
+
result = HostPreflightResult(
|
|
207
|
+
outcome,
|
|
208
|
+
configuration.platform.name if configuration else "Engineering Platform",
|
|
209
|
+
manifest.platform_version if manifest else "unavailable",
|
|
210
|
+
manifest.bootstrap_contract if manifest else "unavailable",
|
|
211
|
+
runtime_path,
|
|
212
|
+
runtime_version,
|
|
213
|
+
timestamp,
|
|
214
|
+
round((monotonic() - started) * 1000),
|
|
215
|
+
tuple(checks),
|
|
216
|
+
drift_evidence,
|
|
217
|
+
guidance(drift_evidence),
|
|
218
|
+
)
|
|
219
|
+
_persist(root, result, run_id)
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def latest(root: Path) -> dict[str, object]:
|
|
224
|
+
"""Return only safe, compact preflight evidence for the dashboard/report."""
|
|
225
|
+
try:
|
|
226
|
+
payload = json.loads((root / ".engineering" / "status" / "host_preflight.json").read_text(encoding="utf-8"))
|
|
227
|
+
except (OSError, json.JSONDecodeError):
|
|
228
|
+
return {}
|
|
229
|
+
if not isinstance(payload, dict):
|
|
230
|
+
return {}
|
|
231
|
+
return {key: payload[key] for key in ("outcome", "timestamp", "duration_ms", "execution_host", "version", "bootstrap_contract", "runtime_path", "runtime_version", "checks", "drift_evidence", "resume_guidance", "run_id") if key in payload}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Crash-safe relocation of the one canonical platform-data directory.
|
|
2
|
+
|
|
3
|
+
The data root is one unit of persistence. Splitting the database and File
|
|
4
|
+
Inbox across arbitrary folders made a restore impossible to reason about, so
|
|
5
|
+
the legacy per-resource relocation requests are deliberately retired.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RelocationError(ValueError):
|
|
16
|
+
"""A requested local relocation cannot be safely completed."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_PENDING = "runtime/pending-platform-data-relocation.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _directory(value: object) -> Path:
|
|
23
|
+
if not isinstance(value, str) or not value.strip():
|
|
24
|
+
raise RelocationError("LOCATION_REQUIRED")
|
|
25
|
+
path = Path(value).expanduser()
|
|
26
|
+
if not path.is_absolute() or not path.is_dir() or not os.access(path, os.W_OK | os.X_OK):
|
|
27
|
+
raise RelocationError("LOCATION_NOT_WRITABLE")
|
|
28
|
+
return path.resolve()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _write_json_atomically(path: Path, value: dict[str, str]) -> None:
|
|
32
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
33
|
+
candidate = path.with_name(f".{path.name}.{uuid4().hex}")
|
|
34
|
+
try:
|
|
35
|
+
candidate.write_text(json.dumps(value, sort_keys=True), encoding="utf-8")
|
|
36
|
+
candidate.chmod(0o600)
|
|
37
|
+
os.replace(candidate, path)
|
|
38
|
+
finally:
|
|
39
|
+
candidate.unlink(missing_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pending_path(data_root: Path) -> Path:
|
|
43
|
+
return data_root.resolve() / _PENDING
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _destination(root: Path, directory: object) -> Path:
|
|
47
|
+
parent = _directory(directory)
|
|
48
|
+
target = parent / root.name
|
|
49
|
+
if target == root or root in target.parents or target in root.parents:
|
|
50
|
+
raise RelocationError("PLATFORM_DATA_DESTINATION_INVALID")
|
|
51
|
+
return target
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def request(data_root: Path, kind: str, directory: object) -> dict[str, str]:
|
|
55
|
+
"""Persist one whole-platform move for the next clean Server startup."""
|
|
56
|
+
if kind != "PLATFORM_DATA":
|
|
57
|
+
raise RelocationError("RELOCATION_KIND_RETIRED")
|
|
58
|
+
root = data_root.resolve()
|
|
59
|
+
if not (root / "engineering.db").is_file():
|
|
60
|
+
raise RelocationError("PLATFORM_DATA_UNAVAILABLE")
|
|
61
|
+
destination = _destination(root, directory)
|
|
62
|
+
if destination.exists() and destination.resolve() != root:
|
|
63
|
+
raise RelocationError("PLATFORM_DATA_DESTINATION_EXISTS")
|
|
64
|
+
pending = _pending_path(root)
|
|
65
|
+
if pending.exists():
|
|
66
|
+
raise RelocationError("RELOCATION_ALREADY_PENDING")
|
|
67
|
+
_write_json_atomically(pending, {"kind": kind, "directory": str(_directory(directory))})
|
|
68
|
+
return {"previous": str(root), "value": str(destination)}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def relocate_platform_data(data_root: Path, directory: object) -> dict[str, str]:
|
|
72
|
+
"""Move the entire data root, retaining its stable launchd entry path.
|
|
73
|
+
|
|
74
|
+
launchd continues to start the stable original path. That path becomes a
|
|
75
|
+
symlink only after the complete directory rename succeeds, so the DB,
|
|
76
|
+
inbox, artifacts and configuration never end up on different volumes.
|
|
77
|
+
"""
|
|
78
|
+
original = Path(data_root).expanduser()
|
|
79
|
+
root = original.resolve()
|
|
80
|
+
parent = _directory(directory)
|
|
81
|
+
# A repeated restart may replay the same request through the stable
|
|
82
|
+
# launchd symlink. Recognize that exact completed move before applying
|
|
83
|
+
# the nested-destination guard below.
|
|
84
|
+
destination = parent / original.name
|
|
85
|
+
if original.is_symlink() and destination.exists() and original.resolve() == destination.resolve():
|
|
86
|
+
return {"previous": str(root), "value": str(destination.resolve())}
|
|
87
|
+
if destination == root or root in destination.parents or destination in root.parents:
|
|
88
|
+
raise RelocationError("PLATFORM_DATA_DESTINATION_INVALID")
|
|
89
|
+
if not root.is_dir() or not (root / "engineering.db").is_file():
|
|
90
|
+
raise RelocationError("PLATFORM_DATA_UNAVAILABLE")
|
|
91
|
+
if destination.exists():
|
|
92
|
+
raise RelocationError("PLATFORM_DATA_DESTINATION_EXISTS")
|
|
93
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
94
|
+
try:
|
|
95
|
+
os.replace(root, destination)
|
|
96
|
+
# A later relocation starts through the stable, already-symlinked
|
|
97
|
+
# launch path. Its old link is now dangling and must be replaced.
|
|
98
|
+
if original.is_symlink():
|
|
99
|
+
original.unlink()
|
|
100
|
+
original.symlink_to(destination, target_is_directory=True)
|
|
101
|
+
except Exception:
|
|
102
|
+
if destination.exists() and not original.exists():
|
|
103
|
+
os.replace(destination, original)
|
|
104
|
+
raise
|
|
105
|
+
return {"previous": str(root), "value": str(destination.resolve())}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def apply_pending(data_root: Path) -> dict[str, str] | None:
|
|
109
|
+
"""Perform a durable whole-directory request before writers are started."""
|
|
110
|
+
pending = _pending_path(data_root)
|
|
111
|
+
if not pending.exists():
|
|
112
|
+
return None
|
|
113
|
+
try:
|
|
114
|
+
request_data = json.loads(pending.read_text(encoding="utf-8"))
|
|
115
|
+
kind, directory = request_data["kind"], request_data["directory"]
|
|
116
|
+
if kind != "PLATFORM_DATA":
|
|
117
|
+
raise RelocationError("RELOCATION_KIND_RETIRED")
|
|
118
|
+
result = relocate_platform_data(data_root, directory)
|
|
119
|
+
except (KeyError, TypeError, json.JSONDecodeError) as error:
|
|
120
|
+
raise RelocationError("RELOCATION_REQUEST_INVALID") from error
|
|
121
|
+
_pending_path(data_root).unlink(missing_ok=True)
|
|
122
|
+
return {**result, "kind": "PLATFORM_DATA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Ephemeral factual-deduplication rules for one primary provider invocation.
|
|
2
|
+
|
|
3
|
+
The ledger deliberately contains fact identifiers and freshness only. It never
|
|
4
|
+
accepts source text, paths, command arguments, tool output, conclusions, or
|
|
5
|
+
reviewer advice, and is rendered only into the primary-provider prompt.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RUN_STABLE = "RUN-STABLE"
|
|
14
|
+
MUTABLE = "MUTABLE"
|
|
15
|
+
BOUNDARY_SENSITIVE = "BOUNDARY-SENSITIVE"
|
|
16
|
+
|
|
17
|
+
_FACT_FRESHNESS = {
|
|
18
|
+
"repository_identity": RUN_STABLE,
|
|
19
|
+
"repository_status": MUTABLE,
|
|
20
|
+
"git_ancestry": MUTABLE,
|
|
21
|
+
"pull_request_state": BOUNDARY_SENSITIVE,
|
|
22
|
+
"source_inspection": MUTABLE,
|
|
23
|
+
"test_surface": MUTABLE,
|
|
24
|
+
"validation_surface": BOUNDARY_SENSITIVE,
|
|
25
|
+
"finalization_state": BOUNDARY_SENSITIVE,
|
|
26
|
+
"reconciliation_state": BOUNDARY_SENSITIVE,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class InvocationInvestigationLedger:
|
|
32
|
+
"""A small, non-persistent checklist of facts usable in one invocation."""
|
|
33
|
+
|
|
34
|
+
completed: frozenset[str] = frozenset()
|
|
35
|
+
|
|
36
|
+
def record(self, *facts: str) -> "InvocationInvestigationLedger":
|
|
37
|
+
"""Record only known fact identifiers after a real narrow check."""
|
|
38
|
+
unknown = set(facts).difference(_FACT_FRESHNESS)
|
|
39
|
+
if unknown:
|
|
40
|
+
raise ValueError("Unknown investigation fact identifier.")
|
|
41
|
+
return InvocationInvestigationLedger(self.completed.union(facts))
|
|
42
|
+
|
|
43
|
+
def reusable(self, fact: str) -> bool:
|
|
44
|
+
"""Return whether the fact is currently established in this invocation."""
|
|
45
|
+
if fact not in _FACT_FRESHNESS:
|
|
46
|
+
raise ValueError("Unknown investigation fact identifier.")
|
|
47
|
+
return fact in self.completed
|
|
48
|
+
|
|
49
|
+
def invalidate(self, boundary: str) -> "InvocationInvestigationLedger":
|
|
50
|
+
"""Fail closed at mutation and lifecycle boundaries.
|
|
51
|
+
|
|
52
|
+
RUN-STABLE identity remains valid. Every other fact must be checked
|
|
53
|
+
again after any boundary that could have changed repository or remote
|
|
54
|
+
state. This is intentionally conservative: a caller may always do a
|
|
55
|
+
real check sooner when freshness is uncertain.
|
|
56
|
+
"""
|
|
57
|
+
if boundary not in {
|
|
58
|
+
"repository_mutation",
|
|
59
|
+
"validation",
|
|
60
|
+
"pull_request_mutation",
|
|
61
|
+
"merge",
|
|
62
|
+
"finalization",
|
|
63
|
+
"repository_cleanup",
|
|
64
|
+
"freshness_uncertain",
|
|
65
|
+
}:
|
|
66
|
+
raise ValueError("Unknown freshness boundary.")
|
|
67
|
+
return InvocationInvestigationLedger(
|
|
68
|
+
frozenset(
|
|
69
|
+
fact for fact in self.completed if _FACT_FRESHNESS[fact] == RUN_STABLE
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def to_prompt_dict(self) -> dict[str, object]:
|
|
74
|
+
"""Return an identifier-only primary prompt projection."""
|
|
75
|
+
return {
|
|
76
|
+
"scope": "one_primary_provider_invocation",
|
|
77
|
+
"persistence": "none",
|
|
78
|
+
"completed_fact_ids": sorted(self.completed),
|
|
79
|
+
"fact_freshness": dict(_FACT_FRESHNESS),
|
|
80
|
+
"invalidating_boundaries": [
|
|
81
|
+
"repository_mutation",
|
|
82
|
+
"validation",
|
|
83
|
+
"pull_request_mutation",
|
|
84
|
+
"merge",
|
|
85
|
+
"finalization",
|
|
86
|
+
"repository_cleanup",
|
|
87
|
+
"freshness_uncertain",
|
|
88
|
+
],
|
|
89
|
+
}
|