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,471 @@
|
|
|
1
|
+
"""Qualified provider contracts and current local implementations.
|
|
2
|
+
|
|
3
|
+
Provider selection is configuration-owned. These protocols deliberately expose
|
|
4
|
+
diagnostics only; they do not grant execution, repository or network authority.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from ipaddress import IPv4Address, IPv4Network
|
|
10
|
+
import os
|
|
11
|
+
import pwd
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Mapping, Protocol, Sequence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ProviderStatus:
|
|
22
|
+
name: str
|
|
23
|
+
version: str
|
|
24
|
+
qualified: bool
|
|
25
|
+
detail: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class LaunchdRuntimeDetails:
|
|
30
|
+
"""Host observation for one owned LaunchAgent, without control authority."""
|
|
31
|
+
|
|
32
|
+
label: str
|
|
33
|
+
loaded: bool
|
|
34
|
+
active: bool
|
|
35
|
+
pid: int | None
|
|
36
|
+
last_exit_code: str | None
|
|
37
|
+
memory_kib: int | None = None
|
|
38
|
+
uptime_seconds: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _elapsed_seconds(value: str) -> int | None:
|
|
42
|
+
"""Parse macOS ``ps etime`` without treating malformed host output as fact."""
|
|
43
|
+
match = re.fullmatch(r"(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})", value)
|
|
44
|
+
if not match:
|
|
45
|
+
return None
|
|
46
|
+
days, hours, minutes, seconds = (int(part or 0) for part in match.groups())
|
|
47
|
+
if minutes >= 60 or seconds >= 60:
|
|
48
|
+
return None
|
|
49
|
+
return days * 86_400 + hours * 3_600 + minutes * 60 + seconds
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RuntimeProvider(Protocol):
|
|
53
|
+
def status(self) -> ProviderStatus: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProcessProvider(Protocol):
|
|
57
|
+
"""The sole boundary for local child-process execution."""
|
|
58
|
+
|
|
59
|
+
def execute(
|
|
60
|
+
self, root: Path, arguments: Sequence[str], *, environment: Mapping[str, str] | None = None,
|
|
61
|
+
) -> subprocess.CompletedProcess[str]: ...
|
|
62
|
+
|
|
63
|
+
def spawn(self, root: Path, arguments: Sequence[str]) -> subprocess.Popen[str]: ...
|
|
64
|
+
|
|
65
|
+
def spawn_detached(self, root: Path, arguments: Sequence[str], environment: Mapping[str, str]) -> subprocess.Popen[bytes]: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LocalProcessProvider:
|
|
69
|
+
"""Default local process adapter; orchestration code never imports subprocess for work."""
|
|
70
|
+
|
|
71
|
+
def execute(
|
|
72
|
+
self, root: Path, arguments: Sequence[str], *, environment: Mapping[str, str] | None = None,
|
|
73
|
+
) -> subprocess.CompletedProcess[str]:
|
|
74
|
+
return subprocess.run(
|
|
75
|
+
arguments, cwd=root, env=dict(environment) if environment is not None else None,
|
|
76
|
+
text=True, capture_output=True, check=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def spawn(self, root: Path, arguments: Sequence[str]) -> subprocess.Popen[str]:
|
|
80
|
+
return subprocess.Popen(
|
|
81
|
+
tuple(arguments), cwd=root, text=True, stdout=subprocess.PIPE,
|
|
82
|
+
stderr=subprocess.STDOUT, start_new_session=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def spawn_detached(self, root: Path, arguments: Sequence[str], environment: Mapping[str, str]) -> subprocess.Popen[bytes]:
|
|
86
|
+
return subprocess.Popen(
|
|
87
|
+
tuple(arguments), cwd=root, env=dict(environment), start_new_session=True,
|
|
88
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def installed_python_environment() -> dict[str, str]:
|
|
93
|
+
"""Return the Server-owned interpreter environment for child validation.
|
|
94
|
+
|
|
95
|
+
An installed Engineering Platform Server is executed by its installation
|
|
96
|
+
virtual environment. Child validation must retain that interpreter when a
|
|
97
|
+
repository asks for the conventional ``python -m ...`` command; otherwise
|
|
98
|
+
it can accidentally resolve a different system Python.
|
|
99
|
+
"""
|
|
100
|
+
# Keep the venv launcher path intact. Resolving it selects the framework
|
|
101
|
+
# base interpreter and lets a child validation command escape the EP venv.
|
|
102
|
+
executable = Path(sys.executable).expanduser().absolute()
|
|
103
|
+
environment = dict(os.environ)
|
|
104
|
+
environment["PATH"] = str(executable.parent) + os.pathsep + environment.get("PATH", "")
|
|
105
|
+
virtual_environment = executable.parent.parent
|
|
106
|
+
if (virtual_environment / "pyvenv.cfg").is_file():
|
|
107
|
+
environment["VIRTUAL_ENV"] = str(virtual_environment)
|
|
108
|
+
return environment
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DeterministicValidationExecutor:
|
|
112
|
+
"""Run one resolved validation control outside provider-agent dispatch."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, process: ProcessProvider | None = None) -> None:
|
|
115
|
+
self.process = process or LocalProcessProvider()
|
|
116
|
+
|
|
117
|
+
def run(self, root: Path, command: tuple[str, ...]) -> "DeterministicValidationResult":
|
|
118
|
+
try:
|
|
119
|
+
completed = self.process.execute(root, command, environment=installed_python_environment())
|
|
120
|
+
stdout = completed.stdout
|
|
121
|
+
stderr = completed.stderr
|
|
122
|
+
return DeterministicValidationResult(
|
|
123
|
+
exit_code=completed.returncode,
|
|
124
|
+
stdout=stdout if isinstance(stdout, str) else None,
|
|
125
|
+
stderr=stderr if isinstance(stderr, str) else None,
|
|
126
|
+
diagnostic_capture_available=isinstance(stdout, str) and isinstance(stderr, str),
|
|
127
|
+
)
|
|
128
|
+
except OSError:
|
|
129
|
+
return DeterministicValidationResult(
|
|
130
|
+
exit_code=None, stdout=None, stderr=None,
|
|
131
|
+
diagnostic_capture_available=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class DeterministicValidationResult:
|
|
137
|
+
"""One deterministic command outcome, including non-authoritative output."""
|
|
138
|
+
|
|
139
|
+
exit_code: int | None
|
|
140
|
+
stdout: str | None
|
|
141
|
+
stderr: str | None
|
|
142
|
+
diagnostic_capture_available: bool
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RepositoryProvider(Protocol):
|
|
146
|
+
def status(self, root: Path) -> ProviderStatus: ...
|
|
147
|
+
def command(self, root: Path, *args: str) -> str: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ServiceManagerProvider(Protocol):
|
|
151
|
+
def status(self) -> ProviderStatus: ...
|
|
152
|
+
def install(self, label: str, plist: Path) -> None: ...
|
|
153
|
+
def uninstall(self, plist: Path) -> None: ...
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RemoteSubmissionProvider(Protocol):
|
|
157
|
+
def status(self) -> ProviderStatus: ...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class PrivateRemoteAccessProvider(Protocol):
|
|
161
|
+
def status(self) -> ProviderStatus: ...
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT = "EP_MANAGED_CODEX_CLI_PREFIX"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def default_engineering_platform_codex_cli_prefix() -> Path:
|
|
168
|
+
"""Return the stable account-owned default, independent of ``$HOME``.
|
|
169
|
+
|
|
170
|
+
A runner may deliberately receive an isolated HOME for tool state. That
|
|
171
|
+
must never manufacture a second EP-managed CLI installation location.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
account_home = Path(pwd.getpwuid(os.getuid()).pw_dir)
|
|
175
|
+
except (KeyError, OSError):
|
|
176
|
+
account_home = Path.home()
|
|
177
|
+
return account_home / ".local" / "share" / "engineering-platform" / "codex-cli"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def engineering_platform_codex_cli_prefix() -> Path:
|
|
181
|
+
"""Return the installation-pinned CLI prefix, never a process HOME path."""
|
|
182
|
+
configured = os.environ.get(MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT)
|
|
183
|
+
if configured:
|
|
184
|
+
candidate = Path(configured).expanduser()
|
|
185
|
+
if candidate.is_absolute():
|
|
186
|
+
return candidate.resolve(strict=False)
|
|
187
|
+
return default_engineering_platform_codex_cli_prefix()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def codex_cli_executable() -> str | None:
|
|
191
|
+
"""Return only Engineering Platform's managed Codex CLI executable."""
|
|
192
|
+
managed = engineering_platform_codex_cli_prefix() / "bin" / "codex"
|
|
193
|
+
if managed.is_file() and os.access(managed, os.X_OK):
|
|
194
|
+
return str(managed)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class CodexCliProvider(LocalProcessProvider):
|
|
199
|
+
"""Codex process adapter pinned exclusively to EP's managed launcher."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, executable: str | None = None) -> None:
|
|
202
|
+
del executable # Runtime injection must not bypass EP's managed CLI.
|
|
203
|
+
self._executable = codex_cli_executable() or ""
|
|
204
|
+
|
|
205
|
+
def managed_installation_path(self) -> str | None:
|
|
206
|
+
"""Return provenance only when this invocation is pinned to EP's CLI."""
|
|
207
|
+
managed = engineering_platform_codex_cli_prefix() / "bin" / "codex"
|
|
208
|
+
return str(engineering_platform_codex_cli_prefix()) if self._executable == str(managed) else None
|
|
209
|
+
|
|
210
|
+
def _arguments(self, arguments: Sequence[str]) -> tuple[str, ...]:
|
|
211
|
+
if arguments and arguments[0] == "codex":
|
|
212
|
+
return (self._executable, *arguments[1:])
|
|
213
|
+
return tuple(arguments)
|
|
214
|
+
|
|
215
|
+
def status(self) -> ProviderStatus:
|
|
216
|
+
available = bool(self._executable) and Path(self._executable).is_file() and os.access(self._executable, os.X_OK)
|
|
217
|
+
return ProviderStatus("codex_cli", "configured", available, "available" if available else "codex unavailable")
|
|
218
|
+
|
|
219
|
+
def command(self, *args: str) -> subprocess.CompletedProcess[str]:
|
|
220
|
+
if not self._executable:
|
|
221
|
+
raise FileNotFoundError("Engineering Platform managed Codex CLI is unavailable")
|
|
222
|
+
return subprocess.run((self._executable, *args), text=True, capture_output=True, check=False)
|
|
223
|
+
|
|
224
|
+
def app_server(self) -> subprocess.Popen[str]:
|
|
225
|
+
"""Open the provider-owned interactive Codex app-server channel."""
|
|
226
|
+
if not self._executable:
|
|
227
|
+
raise FileNotFoundError("Engineering Platform managed Codex CLI is unavailable")
|
|
228
|
+
return subprocess.Popen(
|
|
229
|
+
(self._executable, "app-server"), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
230
|
+
stderr=subprocess.DEVNULL, text=True, bufsize=1,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def close_app_server(self, process: subprocess.Popen[str]) -> None:
|
|
234
|
+
process.terminate()
|
|
235
|
+
try:
|
|
236
|
+
process.wait(timeout=1)
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
process.kill()
|
|
239
|
+
process.wait(timeout=1)
|
|
240
|
+
for stream in (process.stdin, process.stdout):
|
|
241
|
+
if stream is not None:
|
|
242
|
+
stream.close()
|
|
243
|
+
|
|
244
|
+
def invoke(
|
|
245
|
+
self,
|
|
246
|
+
root: Path,
|
|
247
|
+
arguments: tuple[str, ...],
|
|
248
|
+
*,
|
|
249
|
+
timeout: float | None = None,
|
|
250
|
+
environment: Mapping[str, str] | None = None,
|
|
251
|
+
input_text: str | None = None,
|
|
252
|
+
) -> subprocess.CompletedProcess[str]:
|
|
253
|
+
"""Execute a complete Codex command; callers never spawn its CLI directly."""
|
|
254
|
+
command = self._arguments(arguments)
|
|
255
|
+
# Reviewer invocations are bounded advisory work. Primary execution
|
|
256
|
+
# streams through ``spawn`` and does not supply a timeout here.
|
|
257
|
+
if environment is None and input_text is None:
|
|
258
|
+
return self.execute(root, command)
|
|
259
|
+
# The executable is this provider's configured Codex launcher, never a
|
|
260
|
+
# caller-selected command. Remaining values are Codex CLI arguments.
|
|
261
|
+
return subprocess.run(
|
|
262
|
+
(self._executable, *command[1:]), cwd=root,
|
|
263
|
+
env=dict(environment) if environment is not None else None, timeout=timeout,
|
|
264
|
+
text=True, input=input_text, capture_output=True, check=False,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def spawn_invocation(
|
|
268
|
+
self, root: Path, arguments: tuple[str, ...], *, environment: Mapping[str, str] | None = None
|
|
269
|
+
) -> subprocess.Popen[str]:
|
|
270
|
+
command = self._arguments(arguments)
|
|
271
|
+
if environment is None:
|
|
272
|
+
return self.spawn(root, command)
|
|
273
|
+
return subprocess.Popen(
|
|
274
|
+
(self._executable, *command[1:]), cwd=root, env=dict(environment),
|
|
275
|
+
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class GitProvider(LocalProcessProvider):
|
|
280
|
+
"""Local Git provider, deliberately separate from the GitHub API provider."""
|
|
281
|
+
|
|
282
|
+
def execute(self, root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
283
|
+
return super().execute(root, args)
|
|
284
|
+
|
|
285
|
+
def command(self, root: Path, *args: str) -> str:
|
|
286
|
+
"""Run Git and expose its bounded text result to repository orchestration."""
|
|
287
|
+
completed = self.execute(root, *args)
|
|
288
|
+
if completed.returncode:
|
|
289
|
+
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip() or "git command failed")
|
|
290
|
+
return completed.stdout.strip()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class GitHubProvider:
|
|
294
|
+
def status(self, root: Path) -> ProviderStatus:
|
|
295
|
+
remote = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
296
|
+
qualified = remote.returncode == 0 and "github" in remote.stdout.lower()
|
|
297
|
+
return ProviderStatus("github", "configured", qualified, remote.stdout.strip() if qualified else "GitHub origin unavailable")
|
|
298
|
+
|
|
299
|
+
def github(self, *args: str) -> str:
|
|
300
|
+
completed = subprocess.run(("gh", *args), text=True, capture_output=True, check=False)
|
|
301
|
+
if completed.returncode:
|
|
302
|
+
raise RuntimeError(completed.stderr.strip() or "GitHub provider command failed")
|
|
303
|
+
return completed.stdout.strip()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class LaunchdProvider:
|
|
307
|
+
def status(self) -> ProviderStatus:
|
|
308
|
+
available = shutil.which("launchctl") is not None
|
|
309
|
+
return ProviderStatus("launchd", "configured", available, "available" if available else "launchctl unavailable")
|
|
310
|
+
|
|
311
|
+
def install(self, label: str, plist: Path) -> None:
|
|
312
|
+
subprocess.run(("launchctl", "bootout", f"gui/{__import__('os').getuid()}", str(plist)), check=False, capture_output=True)
|
|
313
|
+
subprocess.run(("launchctl", "bootstrap", f"gui/{__import__('os').getuid()}", str(plist)), check=False)
|
|
314
|
+
|
|
315
|
+
def uninstall(self, plist: Path) -> None:
|
|
316
|
+
subprocess.run(("launchctl", "bootout", f"gui/{__import__('os').getuid()}", str(plist)), check=False)
|
|
317
|
+
|
|
318
|
+
def inspect(self, label: str) -> bool:
|
|
319
|
+
executable = shutil.which("launchctl")
|
|
320
|
+
if not executable:
|
|
321
|
+
return False
|
|
322
|
+
return subprocess.run((executable, "print", f"gui/{__import__('os').getuid()}/{label}"), text=True, capture_output=True, check=False).returncode == 0
|
|
323
|
+
|
|
324
|
+
def runtime_status(self, label: str) -> ProviderStatus:
|
|
325
|
+
"""Return whether one owned LaunchAgent has a live service process.
|
|
326
|
+
|
|
327
|
+
``launchctl print`` succeeding only proves that a job remains loaded.
|
|
328
|
+
A KeepAlive job can be loaded while repeatedly exiting, so reporting it
|
|
329
|
+
as healthy would project a stale "active" status to the dashboard.
|
|
330
|
+
"""
|
|
331
|
+
details = self.runtime_details(label)
|
|
332
|
+
if not details.loaded:
|
|
333
|
+
if shutil.which("launchctl") is None:
|
|
334
|
+
return ProviderStatus("launchd", "configured", False, "launchctl unavailable")
|
|
335
|
+
return ProviderStatus("launchd", "configured", False, "LaunchAgent is not loaded")
|
|
336
|
+
if details.active:
|
|
337
|
+
return ProviderStatus("launchd", "configured", True, "LaunchAgent process is active")
|
|
338
|
+
return ProviderStatus(
|
|
339
|
+
"launchd",
|
|
340
|
+
"configured",
|
|
341
|
+
False,
|
|
342
|
+
"LaunchAgent is loaded but has no active process",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def runtime_details(self, label: str) -> LaunchdRuntimeDetails:
|
|
346
|
+
"""Read the actual LaunchAgent host state used by component detail views."""
|
|
347
|
+
executable = shutil.which("launchctl")
|
|
348
|
+
if not executable:
|
|
349
|
+
return LaunchdRuntimeDetails(label, False, False, None, None)
|
|
350
|
+
completed = subprocess.run(
|
|
351
|
+
(executable, "print", f"gui/{os.getuid()}/{label}"),
|
|
352
|
+
text=True,
|
|
353
|
+
capture_output=True,
|
|
354
|
+
check=False,
|
|
355
|
+
)
|
|
356
|
+
if completed.returncode:
|
|
357
|
+
return LaunchdRuntimeDetails(label, False, False, None, None)
|
|
358
|
+
output = completed.stdout
|
|
359
|
+
active_count = re.search(r"(?m)^\s*active count\s*=\s*(\d+)", output)
|
|
360
|
+
pid_match = re.search(r"(?m)^\s*pid\s*=\s*([1-9]\d*)", output)
|
|
361
|
+
last_exit = re.search(r"(?m)^\s*last exit code\s*=\s*(.+)$", output)
|
|
362
|
+
pid = int(pid_match.group(1)) if pid_match else None
|
|
363
|
+
active = (active_count is not None and int(active_count.group(1)) > 0) or pid is not None
|
|
364
|
+
memory_kib, uptime_seconds = self._process_metrics(pid) if active and pid else (None, None)
|
|
365
|
+
return LaunchdRuntimeDetails(
|
|
366
|
+
label, True, active, pid,
|
|
367
|
+
last_exit.group(1).strip() if last_exit else None,
|
|
368
|
+
memory_kib, uptime_seconds,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _process_metrics(pid: int) -> tuple[int | None, int | None]:
|
|
373
|
+
"""Read memory and elapsed time for a launchd-proven process only."""
|
|
374
|
+
executable = shutil.which("ps")
|
|
375
|
+
if not executable:
|
|
376
|
+
return None, None
|
|
377
|
+
completed = subprocess.run(
|
|
378
|
+
(executable, "-o", "rss=", "-o", "etime=", "-p", str(pid)),
|
|
379
|
+
text=True,
|
|
380
|
+
capture_output=True,
|
|
381
|
+
check=False,
|
|
382
|
+
)
|
|
383
|
+
if completed.returncode:
|
|
384
|
+
return None, None
|
|
385
|
+
columns = completed.stdout.strip().split()
|
|
386
|
+
if len(columns) != 2 or not columns[0].isdigit():
|
|
387
|
+
return None, None
|
|
388
|
+
return int(columns[0]), _elapsed_seconds(columns[1])
|
|
389
|
+
|
|
390
|
+
def restart(self, label: str) -> None:
|
|
391
|
+
executable = shutil.which("launchctl")
|
|
392
|
+
if not executable:
|
|
393
|
+
raise OSError("launchctl unavailable")
|
|
394
|
+
completed = subprocess.run((executable, "kickstart", "-k", f"gui/{__import__('os').getuid()}/{label}"), text=True, capture_output=True, check=False)
|
|
395
|
+
if completed.returncode:
|
|
396
|
+
raise OSError(completed.stderr.strip() or "launchd restart failed")
|
|
397
|
+
|
|
398
|
+
def quiesce(self, label: str, plist: Path) -> None:
|
|
399
|
+
"""Temporarily unload one owned LaunchAgent for bounded maintenance."""
|
|
400
|
+
executable = shutil.which("launchctl")
|
|
401
|
+
if not executable:
|
|
402
|
+
raise OSError("launchctl unavailable")
|
|
403
|
+
completed = subprocess.run(
|
|
404
|
+
(executable, "bootout", f"gui/{os.getuid()}", str(plist)),
|
|
405
|
+
text=True,
|
|
406
|
+
capture_output=True,
|
|
407
|
+
check=False,
|
|
408
|
+
)
|
|
409
|
+
if completed.returncode:
|
|
410
|
+
raise OSError(completed.stderr.strip() or "launchd quiesce failed")
|
|
411
|
+
# A process signal is not enough: both observations must prove launchd
|
|
412
|
+
# no longer owns a runnable job, preventing KeepAlive replacement.
|
|
413
|
+
if self.inspect(label) or self.inspect(label):
|
|
414
|
+
raise OSError("LaunchAgent remained loaded after maintenance quiesce")
|
|
415
|
+
|
|
416
|
+
def resume(self, label: str, plist: Path) -> None:
|
|
417
|
+
"""Reload a temporarily quiesced owned LaunchAgent and start it."""
|
|
418
|
+
executable = shutil.which("launchctl")
|
|
419
|
+
if not executable:
|
|
420
|
+
raise OSError("launchctl unavailable")
|
|
421
|
+
completed = subprocess.run(
|
|
422
|
+
(executable, "bootstrap", f"gui/{os.getuid()}", str(plist)),
|
|
423
|
+
text=True,
|
|
424
|
+
capture_output=True,
|
|
425
|
+
check=False,
|
|
426
|
+
)
|
|
427
|
+
if completed.returncode:
|
|
428
|
+
raise OSError(completed.stderr.strip() or "launchd resume failed")
|
|
429
|
+
self.restart(label)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class ICloudInboxProvider:
|
|
433
|
+
def status(self) -> ProviderStatus:
|
|
434
|
+
return ProviderStatus("icloud_inbox", "configured", True, "workspace path is resolved by the watcher")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class TailscaleProvider:
|
|
438
|
+
_TAILSCALE_NETWORK = IPv4Network("100.64.0.0/10")
|
|
439
|
+
|
|
440
|
+
def ipv4_address(self) -> str | None:
|
|
441
|
+
"""Return only the local, routable Tailscale IPv4 address.
|
|
442
|
+
|
|
443
|
+
This is a read-only diagnostic query. It never changes Tailnet
|
|
444
|
+
configuration, ACLs, Funnel, or port-forwarding state.
|
|
445
|
+
"""
|
|
446
|
+
executable = shutil.which("tailscale")
|
|
447
|
+
if not executable:
|
|
448
|
+
return None
|
|
449
|
+
observed = subprocess.run((executable, "ip", "-4"), text=True, capture_output=True, check=False)
|
|
450
|
+
if observed.returncode:
|
|
451
|
+
return None
|
|
452
|
+
for candidate in observed.stdout.splitlines():
|
|
453
|
+
try:
|
|
454
|
+
address = IPv4Address(candidate.strip())
|
|
455
|
+
except ValueError:
|
|
456
|
+
continue
|
|
457
|
+
if address in self._TAILSCALE_NETWORK:
|
|
458
|
+
return str(address)
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
def status(self) -> ProviderStatus:
|
|
462
|
+
executable = shutil.which("tailscale")
|
|
463
|
+
if not executable:
|
|
464
|
+
return ProviderStatus("tailscale", "configured", False, "tailscale unavailable")
|
|
465
|
+
observed = subprocess.run((executable, "status", "--json"), text=True, capture_output=True, check=False)
|
|
466
|
+
return ProviderStatus("tailscale", "configured", observed.returncode == 0, "connected" if observed.returncode == 0 else "not connected")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def registry(root: Path) -> dict[str, ProviderStatus]:
|
|
470
|
+
"""Return the deterministic current-provider registry."""
|
|
471
|
+
return {"runtime": CodexCliProvider().status(), "repository": GitHubProvider().status(root), "service_manager": LaunchdProvider().status(), "remote_submission": ICloudInboxProvider().status(), "private_remote_access": TailscaleProvider().status()}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Deterministic local qualification for Engineering Platform capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from .platform_version import EngineeringPlatformManifest
|
|
12
|
+
from .resources import package_path
|
|
13
|
+
from .platform_api import PlatformConfiguration
|
|
14
|
+
from .providers import CodexCliProvider, GitProvider
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class QualificationScenario:
|
|
19
|
+
capability: str
|
|
20
|
+
expected_behavior: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SCENARIOS = tuple(
|
|
24
|
+
QualificationScenario(
|
|
25
|
+
capability,
|
|
26
|
+
"Deterministic local contract passes; failures report evidence without changing lifecycle authority.",
|
|
27
|
+
)
|
|
28
|
+
for capability in (
|
|
29
|
+
"Repository Initialization",
|
|
30
|
+
"Checkpoint Resume",
|
|
31
|
+
"Implementation Lifecycle",
|
|
32
|
+
"Validation Loop",
|
|
33
|
+
"Repair Loop",
|
|
34
|
+
"Owner Authorization",
|
|
35
|
+
"Ready For Review",
|
|
36
|
+
"Automatic Merge",
|
|
37
|
+
"Repository Reconciliation",
|
|
38
|
+
"Finalization",
|
|
39
|
+
"Repository Cleanup",
|
|
40
|
+
"Engineering Memory",
|
|
41
|
+
"Progress Reporting",
|
|
42
|
+
"Engineering Reports",
|
|
43
|
+
"Capability-aware Reviewers",
|
|
44
|
+
"Diagnostics",
|
|
45
|
+
"BLOCKED Recovery",
|
|
46
|
+
"Failure Recovery",
|
|
47
|
+
"Long-running Transactions",
|
|
48
|
+
"Remote Status Model",
|
|
49
|
+
"Private Dashboard",
|
|
50
|
+
"Repository Handoff",
|
|
51
|
+
"Remote Engineering Readiness",
|
|
52
|
+
"Platform Identity",
|
|
53
|
+
"Workspace Identity",
|
|
54
|
+
"Provider Registry",
|
|
55
|
+
"Capability Registry",
|
|
56
|
+
"Public Platform API",
|
|
57
|
+
"Configuration Hierarchy",
|
|
58
|
+
"Configuration Migration",
|
|
59
|
+
"Provider Compatibility",
|
|
60
|
+
"Extraction Readiness Audit",
|
|
61
|
+
"Repository Bootstrap",
|
|
62
|
+
"Project Template",
|
|
63
|
+
"Workspace Provisioning",
|
|
64
|
+
"Genesis Lifecycle",
|
|
65
|
+
"Strict Inbox Sequencing",
|
|
66
|
+
"Local Engineering Evidence Storage",
|
|
67
|
+
"Component Logging and Read-only Advice",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def execute_qualification(
|
|
73
|
+
root: Path,
|
|
74
|
+
checks: dict[str, bool] | None = None,
|
|
75
|
+
*,
|
|
76
|
+
ep_repository_root: Path | None = None,
|
|
77
|
+
evidence_root: Path | None = None,
|
|
78
|
+
) -> dict[str, object]:
|
|
79
|
+
"""Execute all registered local scenarios and write immutable local evidence.
|
|
80
|
+
|
|
81
|
+
``evidence_root`` is for read-only source checkouts such as the Golden
|
|
82
|
+
scenario. It changes only where the receipt is stored, never the
|
|
83
|
+
repository whose contracts are being checked.
|
|
84
|
+
"""
|
|
85
|
+
started = time.monotonic()
|
|
86
|
+
manifest = EngineeringPlatformManifest.load(
|
|
87
|
+
package_path("ENGINEERING_PLATFORM_VERSION.json")
|
|
88
|
+
)
|
|
89
|
+
supplied = checks or {}
|
|
90
|
+
results = []
|
|
91
|
+
for scenario in SCENARIOS:
|
|
92
|
+
passed = supplied.get(
|
|
93
|
+
scenario.capability,
|
|
94
|
+
_default_check(root, scenario.capability, ep_repository_root=ep_repository_root),
|
|
95
|
+
)
|
|
96
|
+
results.append(
|
|
97
|
+
{
|
|
98
|
+
"capability": scenario.capability,
|
|
99
|
+
"status": "PASS" if passed else "FAIL",
|
|
100
|
+
"duration_ms": 0,
|
|
101
|
+
"diagnostic": None if passed else "Scenario contract failed.",
|
|
102
|
+
"evidence": scenario.expected_behavior,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
passed = sum(item["status"] == "PASS" for item in results)
|
|
106
|
+
report = {
|
|
107
|
+
"engineering_platform_version": manifest.platform_version,
|
|
108
|
+
"repository_version": _repository_version(root),
|
|
109
|
+
"codex_cli_version": _codex_version(),
|
|
110
|
+
"qualification": "PASS" if passed == len(results) else "FAIL",
|
|
111
|
+
"executed_at": datetime.now(timezone.utc).isoformat(),
|
|
112
|
+
"duration_ms": round((time.monotonic() - started) * 1000),
|
|
113
|
+
"scenarios": results,
|
|
114
|
+
"coverage_percent": round(passed * 100 / len(results), 1),
|
|
115
|
+
"failures": len(results) - passed,
|
|
116
|
+
"blocked": 0,
|
|
117
|
+
}
|
|
118
|
+
_write_report(evidence_root or root, report)
|
|
119
|
+
return report
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def dashboard(report: dict[str, object]) -> str:
|
|
123
|
+
scenarios = report["scenarios"]
|
|
124
|
+
return "\n".join(
|
|
125
|
+
(
|
|
126
|
+
"Engineering Platform Qualification",
|
|
127
|
+
f"Version: {report['engineering_platform_version']}",
|
|
128
|
+
f"Qualification: {report['qualification']}",
|
|
129
|
+
f"Scenarios: {sum(item['status'] == 'PASS' for item in scenarios)} / {len(scenarios)}",
|
|
130
|
+
f"Failures: {report['failures']}",
|
|
131
|
+
f"Blocked: {report['blocked']}",
|
|
132
|
+
f"Coverage: {report['coverage_percent']}%",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def latest_qualification(root: Path) -> dict[str, object] | None:
|
|
138
|
+
directory = root / ".engineering" / "qualification"
|
|
139
|
+
reports = sorted(directory.glob("qualification-*.json"))
|
|
140
|
+
if not reports:
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
raw = json.loads(reports[-1].read_text(encoding="utf-8"))
|
|
144
|
+
return raw if isinstance(raw, dict) else None
|
|
145
|
+
except (OSError, json.JSONDecodeError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _default_check(
|
|
150
|
+
root: Path, capability: str, *, ep_repository_root: Path | None = None
|
|
151
|
+
) -> bool:
|
|
152
|
+
ep_source = (
|
|
153
|
+
ep_repository_root / "src" / "engineering_platform"
|
|
154
|
+
if ep_repository_root is not None
|
|
155
|
+
else None
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def source_file(name: str) -> bool:
|
|
159
|
+
return bool(ep_source and (ep_source / name).is_file())
|
|
160
|
+
|
|
161
|
+
def configuration_is_compatible() -> bool:
|
|
162
|
+
try:
|
|
163
|
+
return PlatformConfiguration.load(root).platform.version == EngineeringPlatformManifest.load(
|
|
164
|
+
package_path("ENGINEERING_PLATFORM_VERSION.json")
|
|
165
|
+
).platform_version
|
|
166
|
+
except (OSError, ValueError):
|
|
167
|
+
return False
|
|
168
|
+
contracts = {
|
|
169
|
+
"Repository Initialization": bool(ep_repository_root and (ep_repository_root / "BOOTSTRAP.md").is_file()),
|
|
170
|
+
"Checkpoint Resume": source_file("agent_state.py"),
|
|
171
|
+
"Engineering Memory": source_file("execution_host.py"),
|
|
172
|
+
"Capability-aware Reviewers": source_file("capability_review.py"),
|
|
173
|
+
"Remote Status Model": source_file("status_model.py"),
|
|
174
|
+
"Private Dashboard": source_file("server.py") and source_file("console_presentation.py"),
|
|
175
|
+
"Repository Handoff": source_file("repository_handoff.py"),
|
|
176
|
+
"Remote Engineering Readiness": source_file("execution_readiness.py"),
|
|
177
|
+
"Platform Identity": configuration_is_compatible(),
|
|
178
|
+
"Workspace Identity": configuration_is_compatible(),
|
|
179
|
+
"Provider Registry": configuration_is_compatible(),
|
|
180
|
+
"Capability Registry": source_file("platform_api.py"),
|
|
181
|
+
"Public Platform API": source_file("platform_api.py"),
|
|
182
|
+
"Configuration Hierarchy": configuration_is_compatible(),
|
|
183
|
+
"Configuration Migration": configuration_is_compatible(),
|
|
184
|
+
"Provider Compatibility": configuration_is_compatible(),
|
|
185
|
+
"Extraction Readiness Audit": bool(
|
|
186
|
+
ep_repository_root
|
|
187
|
+
and (ep_repository_root / "scripts" / "engineering" / "audit_ep_extraction_baseline.py").is_file()
|
|
188
|
+
),
|
|
189
|
+
"Repository Bootstrap": source_file("platform_bootstrap.py"),
|
|
190
|
+
"Project Template": bool(ep_source and (ep_source / "templates" / "workspace-config.json").is_file()),
|
|
191
|
+
"Workspace Provisioning": source_file("platform_bootstrap.py"),
|
|
192
|
+
"Genesis Lifecycle": source_file("execution_host.py"),
|
|
193
|
+
"Strict Inbox Sequencing": source_file("file_inbox.py"),
|
|
194
|
+
"Local Engineering Evidence Storage": source_file("file_inbox.py") and source_file("submission_intake.py"),
|
|
195
|
+
"Component Logging and Read-only Advice": source_file("component_logging.py") and source_file("codex_chat.py"),
|
|
196
|
+
}
|
|
197
|
+
return contracts.get(capability, source_file("execution_host.py"))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _write_report(root: Path, report: dict[str, object]) -> None:
|
|
201
|
+
directory = root / ".engineering" / "qualification"
|
|
202
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
203
|
+
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
|
|
204
|
+
(directory / f"qualification-{stamp}.json").write_text(
|
|
205
|
+
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
206
|
+
)
|
|
207
|
+
(directory / f"qualification-{stamp}.md").write_text(dashboard(report) + "\n", encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _repository_version(root: Path) -> str:
|
|
211
|
+
completed = GitProvider().execute(root, "git", "rev-parse", "HEAD")
|
|
212
|
+
return completed.stdout.strip() if completed.returncode == 0 else "unavailable"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _codex_version() -> str:
|
|
216
|
+
try:
|
|
217
|
+
completed = CodexCliProvider().command("--version")
|
|
218
|
+
except OSError:
|
|
219
|
+
return "unavailable"
|
|
220
|
+
return completed.stdout.strip() if completed.returncode == 0 else "unavailable"
|