engineering-platform 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Redacted component logging with CENTRAL as the only persistent authority."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import signal
|
|
12
|
+
import sqlite3
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
15
|
+
|
|
16
|
+
from .agent_state import redact_diagnostic
|
|
17
|
+
from .storage import EngineeringStorageError, open_storage
|
|
18
|
+
from .providers import GitProvider
|
|
19
|
+
from .platform_components import PLATFORM_COMPONENT_IDS
|
|
20
|
+
|
|
21
|
+
LOG_LEVEL_ENVIRONMENT = "ENGINEERING_PLATFORM_LOG_LEVEL"
|
|
22
|
+
SERVER_DATA_ROOT_ENVIRONMENT = "EP_SERVER_DATA_ROOT"
|
|
23
|
+
DEFAULT_LOG_LEVEL = "INFO"
|
|
24
|
+
COMPONENT_LOG_PAGE_SIZE = 50
|
|
25
|
+
MAX_COMPONENT_LOG_PAGE_SIZE = 200
|
|
26
|
+
PLATFORM_LOG_COMPONENTS = PLATFORM_COMPONENT_IDS
|
|
27
|
+
VALID_LEVELS = frozenset({"DEBUG", "INFO", "WARNING", "ERROR"})
|
|
28
|
+
LOG_LEVELS_AT_OR_ABOVE = {
|
|
29
|
+
"INFO": ("INFO", "WARNING", "ERROR"),
|
|
30
|
+
"WARNING": ("WARNING", "ERROR"),
|
|
31
|
+
"ERROR": ("ERROR",),
|
|
32
|
+
}
|
|
33
|
+
LIFECYCLE_CONTEXT_KEYS = frozenset(
|
|
34
|
+
{
|
|
35
|
+
"application_version",
|
|
36
|
+
"git_commit",
|
|
37
|
+
"launchd_label",
|
|
38
|
+
"launch_agent_path",
|
|
39
|
+
"target_component",
|
|
40
|
+
"shutdown_signal",
|
|
41
|
+
"configuration_scope",
|
|
42
|
+
"configuration_key",
|
|
43
|
+
"previous_value",
|
|
44
|
+
"new_value",
|
|
45
|
+
"provider",
|
|
46
|
+
"provider_action",
|
|
47
|
+
"provider_action_source",
|
|
48
|
+
"audit_outcome",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def configured_level(value: str | None = None) -> int:
|
|
54
|
+
"""Resolve the supported local logging level without silently accepting typos."""
|
|
55
|
+
name = (value or os.environ.get(LOG_LEVEL_ENVIRONMENT, DEFAULT_LOG_LEVEL)).upper()
|
|
56
|
+
return getattr(logging, name if name in VALID_LEVELS else DEFAULT_LOG_LEVEL)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RedactingJsonFormatter(logging.Formatter):
|
|
60
|
+
"""Persist only bounded, redacted structured component events."""
|
|
61
|
+
|
|
62
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
63
|
+
payload: dict[str, object] = {
|
|
64
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
65
|
+
"level": record.levelname,
|
|
66
|
+
"component": redact_diagnostic(str(getattr(record, "component", record.name)), limit=80),
|
|
67
|
+
"run_id": redact_diagnostic(str(getattr(record, "run_id", "")), limit=80) or None,
|
|
68
|
+
"event": redact_diagnostic(record.getMessage(), limit=500),
|
|
69
|
+
}
|
|
70
|
+
diagnostic = getattr(record, "diagnostic", None)
|
|
71
|
+
if diagnostic is not None:
|
|
72
|
+
payload["diagnostic"] = redact_diagnostic(str(diagnostic), limit=500)
|
|
73
|
+
context = getattr(record, "context", {})
|
|
74
|
+
if isinstance(context, Mapping):
|
|
75
|
+
for key in sorted(LIFECYCLE_CONTEXT_KEYS):
|
|
76
|
+
if (value := context.get(key)) is not None:
|
|
77
|
+
payload[key] = redact_diagnostic(str(value), limit=500)
|
|
78
|
+
return json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SQLiteLogHandler(logging.Handler):
|
|
82
|
+
"""Persist component events only in the canonical CENTRAL store."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, root: Path, component: str, *, central_database: Path | None = None) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self.root = root.resolve()
|
|
87
|
+
self.component = component
|
|
88
|
+
self.central_database = central_database.resolve() if central_database is not None else None
|
|
89
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
90
|
+
try:
|
|
91
|
+
if self.central_database is None:
|
|
92
|
+
raise EngineeringStorageError("CENTRAL component log authority is unavailable")
|
|
93
|
+
payload = self.format(record)
|
|
94
|
+
parsed = json.loads(payload)
|
|
95
|
+
created_at = parsed.get("timestamp")
|
|
96
|
+
connection = sqlite3.connect(self.central_database, isolation_level=None)
|
|
97
|
+
try:
|
|
98
|
+
connection.execute(
|
|
99
|
+
"INSERT INTO engineering_component_logs(component,payload,created_at) VALUES(?,?,?)",
|
|
100
|
+
(self.component, payload, created_at),
|
|
101
|
+
)
|
|
102
|
+
finally:
|
|
103
|
+
connection.close()
|
|
104
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError, TypeError, ValueError):
|
|
105
|
+
# Falling back into an arbitrary checkout would silently create a
|
|
106
|
+
# second supported component-log authority. Keep the diagnostic
|
|
107
|
+
# observable but non-persistent when CENTRAL is unavailable.
|
|
108
|
+
try:
|
|
109
|
+
sys.stderr.write("engineering-platform: CENTRAL component log unavailable\n")
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
super().close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def component_logger(
|
|
118
|
+
root: Path, component: str, *, level: str | None = None,
|
|
119
|
+
central_database: Path | None = None,
|
|
120
|
+
) -> logging.Logger:
|
|
121
|
+
"""Return the canonical CENTRAL logger for supported Platform components.
|
|
122
|
+
|
|
123
|
+
Installed runtime writers do not select a repository-local sink: the
|
|
124
|
+
Server publishes its data root and the component identity resolves through
|
|
125
|
+
the shared Platform Component model. Missing CENTRAL binding is a
|
|
126
|
+
bounded diagnostic, never a local persistent fallback.
|
|
127
|
+
"""
|
|
128
|
+
if component not in PLATFORM_COMPONENT_IDS:
|
|
129
|
+
# The database table has no foreign key to the component model. The
|
|
130
|
+
# writer is therefore the authority boundary: accepting an arbitrary
|
|
131
|
+
# string here would let a retired alias become a new operational log
|
|
132
|
+
# identity simply by supplying a CENTRAL path.
|
|
133
|
+
raise ValueError("Unsupported Platform component.")
|
|
134
|
+
if central_database is None:
|
|
135
|
+
configured_root = os.environ.get(SERVER_DATA_ROOT_ENVIRONMENT)
|
|
136
|
+
if configured_root:
|
|
137
|
+
candidate = Path(configured_root).resolve() / "engineering.db"
|
|
138
|
+
if candidate.is_file():
|
|
139
|
+
central_database = candidate
|
|
140
|
+
logger = logging.getLogger(f"engineering_platform.{component}")
|
|
141
|
+
logger.setLevel(configured_level(level))
|
|
142
|
+
logger.propagate = False
|
|
143
|
+
for handler in tuple(logger.handlers):
|
|
144
|
+
if (
|
|
145
|
+
isinstance(handler, SQLiteLogHandler)
|
|
146
|
+
and handler.root == root.resolve()
|
|
147
|
+
and handler.central_database == (central_database.resolve() if central_database is not None else None)
|
|
148
|
+
and handler.component == component
|
|
149
|
+
):
|
|
150
|
+
handler.setLevel(logger.level)
|
|
151
|
+
return logger
|
|
152
|
+
logger.removeHandler(handler)
|
|
153
|
+
handler.close()
|
|
154
|
+
handler = SQLiteLogHandler(root, component, central_database=central_database)
|
|
155
|
+
handler.setLevel(logger.level)
|
|
156
|
+
handler.setFormatter(RedactingJsonFormatter())
|
|
157
|
+
logger.addHandler(handler)
|
|
158
|
+
return logger
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def component_log(root: Path, component: str, *, limit: int = 100) -> bytes:
|
|
162
|
+
"""Read component logs from CENTRAL, never from a repository-local fallback."""
|
|
163
|
+
if component not in PLATFORM_COMPONENT_IDS:
|
|
164
|
+
return b""
|
|
165
|
+
try:
|
|
166
|
+
connection = open_storage(root)
|
|
167
|
+
try:
|
|
168
|
+
rows = connection.execute(
|
|
169
|
+
"SELECT payload FROM engineering_component_logs WHERE component=? ORDER BY id DESC LIMIT ?",
|
|
170
|
+
(component, limit),
|
|
171
|
+
).fetchall()
|
|
172
|
+
finally:
|
|
173
|
+
connection.close()
|
|
174
|
+
lines = [str(row[0]) for row in reversed(rows)]
|
|
175
|
+
return ("\n".join(lines) or "Nog geen applicatielog beschikbaar.").encode()
|
|
176
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError):
|
|
177
|
+
return b"CENTRAL componentlog is tijdelijk niet beschikbaar."
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def component_log_page(
|
|
181
|
+
root: Path,
|
|
182
|
+
component: str,
|
|
183
|
+
*,
|
|
184
|
+
page: int = 1,
|
|
185
|
+
page_size: int = COMPONENT_LOG_PAGE_SIZE,
|
|
186
|
+
start_at: str | None = None,
|
|
187
|
+
end_at: str | None = None,
|
|
188
|
+
inclusive_end: bool = False,
|
|
189
|
+
search: str = "",
|
|
190
|
+
level: str = "",
|
|
191
|
+
events: Iterable[str] = (),
|
|
192
|
+
sort_key: str = "timestamp",
|
|
193
|
+
direction: str = "desc",
|
|
194
|
+
) -> dict[str, object]:
|
|
195
|
+
"""Return one bounded, filtered page from canonical component-log storage.
|
|
196
|
+
|
|
197
|
+
Filtering happens before pagination so a historical date, event or text
|
|
198
|
+
search never disappears merely because newer rows filled an arbitrary
|
|
199
|
+
client-side sample.
|
|
200
|
+
"""
|
|
201
|
+
if component not in PLATFORM_COMPONENT_IDS:
|
|
202
|
+
raise ValueError("Onbekende componentlog.")
|
|
203
|
+
if not isinstance(page, int) or page < 1:
|
|
204
|
+
raise ValueError("Ongeldige logpagina.")
|
|
205
|
+
if not isinstance(page_size, int) or not 1 <= page_size <= MAX_COMPONENT_LOG_PAGE_SIZE:
|
|
206
|
+
raise ValueError("Ongeldige logpaginagrootte.")
|
|
207
|
+
normalized_level = level.upper().strip()
|
|
208
|
+
if normalized_level and normalized_level not in VALID_LEVELS:
|
|
209
|
+
raise ValueError("Ongeldig logniveau.")
|
|
210
|
+
normalized_search = str(search).strip()
|
|
211
|
+
if len(normalized_search) > 160:
|
|
212
|
+
raise ValueError("Zoekterm voor logs is te lang.")
|
|
213
|
+
normalized_events = tuple(
|
|
214
|
+
sorted({str(event).strip() for event in events if str(event).strip()})
|
|
215
|
+
)
|
|
216
|
+
if len(normalized_events) > 50 or any(len(event) > 160 for event in normalized_events):
|
|
217
|
+
raise ValueError("Ongeldige gebeurtenisfilter.")
|
|
218
|
+
sort_columns = {
|
|
219
|
+
"line": "id",
|
|
220
|
+
"timestamp": "created_at",
|
|
221
|
+
"level": "json_extract(payload, '$.level')",
|
|
222
|
+
"event": "json_extract(payload, '$.event')",
|
|
223
|
+
"runId": "COALESCE(json_extract(payload, '$.run_id'), '')",
|
|
224
|
+
"details": "COALESCE(json_extract(payload, '$.diagnostic'), '')",
|
|
225
|
+
}
|
|
226
|
+
if sort_key not in sort_columns or direction not in {"asc", "desc"}:
|
|
227
|
+
raise ValueError("Ongeldige logsorteervolgorde.")
|
|
228
|
+
|
|
229
|
+
clauses = ["component=?"]
|
|
230
|
+
parameters: list[object] = [component]
|
|
231
|
+
if start_at:
|
|
232
|
+
clauses.append("created_at >= ?")
|
|
233
|
+
parameters.append(start_at)
|
|
234
|
+
if end_at:
|
|
235
|
+
clauses.append("created_at <= ?" if inclusive_end else "created_at < ?")
|
|
236
|
+
parameters.append(end_at)
|
|
237
|
+
if normalized_search:
|
|
238
|
+
escaped = normalized_search.lower().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
239
|
+
clauses.append("LOWER(payload) LIKE ? ESCAPE '\\'")
|
|
240
|
+
parameters.append(f"%{escaped}%")
|
|
241
|
+
# The dashboard's level picker is a minimum-severity filter. DEBUG is
|
|
242
|
+
# deliberately unbounded so it includes every retained record, including
|
|
243
|
+
# a future or legacy level the dashboard does not yet classify.
|
|
244
|
+
if normalized_level in LOG_LEVELS_AT_OR_ABOVE:
|
|
245
|
+
levels = LOG_LEVELS_AT_OR_ABOVE[normalized_level]
|
|
246
|
+
clauses.append("json_extract(payload, '$.level') IN (" + ",".join("?" for _ in levels) + ")")
|
|
247
|
+
parameters.extend(levels)
|
|
248
|
+
event_option_clauses = list(clauses)
|
|
249
|
+
event_option_parameters = list(parameters)
|
|
250
|
+
if normalized_events:
|
|
251
|
+
clauses.append("json_extract(payload, '$.event') IN (" + ",".join("?" for _ in normalized_events) + ")")
|
|
252
|
+
parameters.extend(normalized_events)
|
|
253
|
+
where = " WHERE " + " AND ".join(clauses)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
connection = open_storage(root)
|
|
257
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError):
|
|
258
|
+
# Schema activation is deliberately deferred during a managed run.
|
|
259
|
+
# The dashboard remains a read-only consumer, so it must retain its
|
|
260
|
+
# bounded response contract rather than terminate the HTTP handler.
|
|
261
|
+
return {
|
|
262
|
+
"entries": [],
|
|
263
|
+
"page": page,
|
|
264
|
+
"page_size": page_size,
|
|
265
|
+
"total": 0,
|
|
266
|
+
"events": [],
|
|
267
|
+
}
|
|
268
|
+
try:
|
|
269
|
+
total = int(connection.execute(
|
|
270
|
+
"SELECT COUNT(*) FROM engineering_component_logs" + where,
|
|
271
|
+
parameters,
|
|
272
|
+
).fetchone()[0])
|
|
273
|
+
rows = connection.execute(
|
|
274
|
+
"SELECT id,payload FROM engineering_component_logs" + where
|
|
275
|
+
+ f" ORDER BY {sort_columns[sort_key]} {direction.upper()}, id {direction.upper()} LIMIT ? OFFSET ?",
|
|
276
|
+
[*parameters, page_size, (page - 1) * page_size],
|
|
277
|
+
).fetchall()
|
|
278
|
+
event_rows = connection.execute(
|
|
279
|
+
"SELECT DISTINCT json_extract(payload, '$.event') FROM engineering_component_logs WHERE "
|
|
280
|
+
+ " AND ".join(event_option_clauses)
|
|
281
|
+
+ " AND json_extract(payload, '$.event') IS NOT NULL ORDER BY 1 LIMIT 500",
|
|
282
|
+
event_option_parameters,
|
|
283
|
+
).fetchall()
|
|
284
|
+
records: list[dict[str, object]] = []
|
|
285
|
+
for row_id, raw in rows:
|
|
286
|
+
try:
|
|
287
|
+
record = json.loads(str(raw))
|
|
288
|
+
except (TypeError, json.JSONDecodeError):
|
|
289
|
+
continue
|
|
290
|
+
if isinstance(record, dict):
|
|
291
|
+
record["line"] = int(row_id)
|
|
292
|
+
records.append(record)
|
|
293
|
+
return {
|
|
294
|
+
"entries": records,
|
|
295
|
+
"page": page,
|
|
296
|
+
"page_size": page_size,
|
|
297
|
+
"total": total,
|
|
298
|
+
"events": [str(row[0]) for row in event_rows if row[0]],
|
|
299
|
+
}
|
|
300
|
+
finally:
|
|
301
|
+
connection.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def component_log_version(root: Path, component: str) -> str:
|
|
305
|
+
"""Return a lightweight CENTRAL SQLite revision."""
|
|
306
|
+
if component not in PLATFORM_COMPONENT_IDS:
|
|
307
|
+
return "missing"
|
|
308
|
+
try:
|
|
309
|
+
connection = open_storage(root)
|
|
310
|
+
try:
|
|
311
|
+
count, newest = connection.execute(
|
|
312
|
+
"SELECT COUNT(*), COALESCE(MAX(id), 0) FROM engineering_component_logs WHERE component=?",
|
|
313
|
+
(component,),
|
|
314
|
+
).fetchone()
|
|
315
|
+
finally:
|
|
316
|
+
connection.close()
|
|
317
|
+
return f"sqlite:{count}:{newest}"
|
|
318
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError):
|
|
319
|
+
return "central-unavailable"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def clear_component_log(root: Path, component: str) -> None:
|
|
323
|
+
"""Clear one CENTRAL component log without creating local log state."""
|
|
324
|
+
if component not in PLATFORM_COMPONENT_IDS:
|
|
325
|
+
raise ValueError("Onbekende componentlog.")
|
|
326
|
+
try:
|
|
327
|
+
connection = open_storage(root)
|
|
328
|
+
try:
|
|
329
|
+
connection.execute("DELETE FROM engineering_component_logs WHERE component=?", (component,))
|
|
330
|
+
finally:
|
|
331
|
+
connection.close()
|
|
332
|
+
return
|
|
333
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError) as error:
|
|
334
|
+
raise OSError("CENTRAL componentlog kon niet worden gewist.") from error
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def prune_component_logs(root: Path, retention_days: int) -> None:
|
|
338
|
+
"""Remove only expired, local component-log rows for an approved retention period."""
|
|
339
|
+
if retention_days not in {30, 60, 90, 120, 180, 360}:
|
|
340
|
+
raise ValueError("Ongeldige logbewaartermijn.")
|
|
341
|
+
cutoff = datetime.now(timezone.utc).timestamp() - retention_days * 86_400
|
|
342
|
+
cutoff_iso = datetime.fromtimestamp(cutoff, timezone.utc).isoformat()
|
|
343
|
+
connection = open_storage(root)
|
|
344
|
+
try:
|
|
345
|
+
connection.execute(
|
|
346
|
+
"DELETE FROM engineering_component_logs WHERE created_at < ?", (cutoff_iso,)
|
|
347
|
+
)
|
|
348
|
+
finally:
|
|
349
|
+
connection.close()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def log_event(
|
|
353
|
+
logger: logging.Logger,
|
|
354
|
+
level: int,
|
|
355
|
+
event: str,
|
|
356
|
+
*,
|
|
357
|
+
run_id: str | None = None,
|
|
358
|
+
diagnostic: str | None = None,
|
|
359
|
+
context: Mapping[str, object] | None = None,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Write one redacted, structured event without exposing arbitrary extras."""
|
|
362
|
+
logger.log(
|
|
363
|
+
level,
|
|
364
|
+
redact_diagnostic(event, limit=500),
|
|
365
|
+
extra={
|
|
366
|
+
"component": logger.name.rsplit(".", 1)[-1],
|
|
367
|
+
"run_id": run_id or "",
|
|
368
|
+
"diagnostic": diagnostic,
|
|
369
|
+
"context": dict(context or {}),
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def component_lifecycle_context(
|
|
375
|
+
root: Path,
|
|
376
|
+
*,
|
|
377
|
+
version: str,
|
|
378
|
+
launchd_label: str,
|
|
379
|
+
launch_agent_path: Path,
|
|
380
|
+
) -> dict[str, str]:
|
|
381
|
+
"""Return bounded, non-secret identity data for a component lifecycle event."""
|
|
382
|
+
try:
|
|
383
|
+
result = GitProvider().execute(root.resolve(), "git", "rev-parse", "--short=12", "HEAD")
|
|
384
|
+
commit = result.stdout.strip() if result.returncode == 0 else "onbekend"
|
|
385
|
+
except OSError:
|
|
386
|
+
commit = "onbekend"
|
|
387
|
+
return {
|
|
388
|
+
"application_version": version,
|
|
389
|
+
"git_commit": commit or "onbekend",
|
|
390
|
+
"launchd_label": launchd_label,
|
|
391
|
+
"launch_agent_path": str(launch_agent_path),
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@contextmanager
|
|
396
|
+
def shutdown_signal_logging(
|
|
397
|
+
logger: logging.Logger,
|
|
398
|
+
context: Mapping[str, object],
|
|
399
|
+
) -> Iterator[None]:
|
|
400
|
+
"""Log a managed shutdown request before ending the local service loop."""
|
|
401
|
+
previous_handlers: dict[int, signal.Handlers] = {}
|
|
402
|
+
|
|
403
|
+
def request_shutdown(signum: int, _frame: object) -> None:
|
|
404
|
+
signal_name = signal.Signals(signum).name
|
|
405
|
+
log_event(
|
|
406
|
+
logger,
|
|
407
|
+
logging.INFO,
|
|
408
|
+
"component_shutdown_trigger_received",
|
|
409
|
+
context={**context, "shutdown_signal": signal_name},
|
|
410
|
+
)
|
|
411
|
+
raise KeyboardInterrupt
|
|
412
|
+
|
|
413
|
+
for current_signal in (signal.SIGINT, signal.SIGTERM):
|
|
414
|
+
previous_handlers[current_signal] = signal.getsignal(current_signal)
|
|
415
|
+
signal.signal(current_signal, request_shutdown)
|
|
416
|
+
try:
|
|
417
|
+
yield
|
|
418
|
+
finally:
|
|
419
|
+
for current_signal, previous in previous_handlers.items():
|
|
420
|
+
signal.signal(current_signal, previous)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Installed EP Server Console presentation resources.
|
|
2
|
+
|
|
3
|
+
This module owns only package resources and stable presentation identifiers.
|
|
4
|
+
It has no listener, route, project, queue, execution or persistence authority.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ASSET_DIRECTORY = Path(__file__).with_name("assets")
|
|
12
|
+
APP_ICON_DARK = "operations-console/apple-touch-icon-dark.png"
|
|
13
|
+
APP_ICON_LIGHT = "operations-console/apple-touch-icon-light.png"
|
|
14
|
+
WEB_MANIFEST = "operations-console/manifest.webmanifest"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Canonical ownership for Console and Console-adjacent HTTP routes.
|
|
2
|
+
|
|
3
|
+
Ownership is an authority decision, not a presentation decision. A selected
|
|
4
|
+
project may tailor a PLATFORM projection, but never its authority.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from .platform_components import PLATFORM_COMPONENT_ROUTE_PATTERN, RETIRED_COMPONENT_ALIAS_ROUTE_PATTERN
|
|
12
|
+
|
|
13
|
+
PLATFORM = "PLATFORM"
|
|
14
|
+
PROJECT = "PROJECT"
|
|
15
|
+
HOST_ADMIN = "HOST_ADMIN"
|
|
16
|
+
TRANSPORT_INTERNAL = "TRANSPORT_INTERNAL"
|
|
17
|
+
HISTORICAL_UNREACHABLE = "HISTORICAL_UNREACHABLE"
|
|
18
|
+
OWNERS = frozenset({PLATFORM, PROJECT, HOST_ADMIN, TRANSPORT_INTERNAL, HISTORICAL_UNREACHABLE})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ConsoleRoute:
|
|
23
|
+
methods: tuple[str, ...]
|
|
24
|
+
pattern: str
|
|
25
|
+
owner: str
|
|
26
|
+
component: str
|
|
27
|
+
description: str
|
|
28
|
+
selected_project_presentation_only: bool = False
|
|
29
|
+
|
|
30
|
+
def matches(self, method: str, path: str) -> bool:
|
|
31
|
+
return method.upper() in self.methods and re.fullmatch(self.pattern, path) is not None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Closed list: each addition must select an owner and component family.
|
|
35
|
+
ROUTE_OWNERSHIP_MATRIX: tuple[ConsoleRoute, ...] = (
|
|
36
|
+
ConsoleRoute(("GET",), r"/", PLATFORM, "console_shell", "Console shell", True),
|
|
37
|
+
ConsoleRoute(("GET",), r"/(?:favicon\.ico|apple-touch-icon(?:-precomposed)?\.png)", PLATFORM, "console_shell", "Console icons"),
|
|
38
|
+
ConsoleRoute(("GET",), r"/assets/[A-Za-z0-9_./-]+", PLATFORM, "console_shell", "Installed Console assets"),
|
|
39
|
+
ConsoleRoute(("GET",), r"/health", PLATFORM, "platform_components", "Platform Components health", True),
|
|
40
|
+
ConsoleRoute(("GET",), r"/api/platform-status", PLATFORM, "platform_components", "Platform Components projection", True),
|
|
41
|
+
ConsoleRoute(("GET",), r"/api/(?:dashboard-snapshot|status|events)", PLATFORM, "platform_components", "Platform status projection or stream", True),
|
|
42
|
+
ConsoleRoute(("GET",), rf"/api/components/{PLATFORM_COMPONENT_ROUTE_PATTERN}/details", PLATFORM, "platform_components", "Component detail popout", True),
|
|
43
|
+
ConsoleRoute(("POST",), rf"/api/components/{PLATFORM_COMPONENT_ROUTE_PATTERN}/restart", PLATFORM, "platform_components", "Canonical component restart", True),
|
|
44
|
+
ConsoleRoute(("GET", "POST"), rf"/api/logs/(?:all|{PLATFORM_COMPONENT_ROUTE_PATTERN})", PLATFORM, "platform_components", "Platform component logs", True),
|
|
45
|
+
ConsoleRoute(("GET",), r"/api/provider-login-status", PLATFORM, "provider_login", "Provider readiness", True),
|
|
46
|
+
ConsoleRoute(("POST",), r"/api/provider-login/(?:repair|logout)", PLATFORM, "provider_login", "Provider login action", True),
|
|
47
|
+
ConsoleRoute(("GET",), r"/api/execution-runtime-status", PLATFORM, "execution_runtime", "Execution runtime readiness", True),
|
|
48
|
+
ConsoleRoute(("POST",), r"/api/execution-runtime/repair", PLATFORM, "execution_runtime", "Execution runtime repair", True),
|
|
49
|
+
ConsoleRoute(("GET",), r"/api/provider-capacity", PLATFORM, "provider_capacity", "Provider capacity", True),
|
|
50
|
+
ConsoleRoute(("GET", "POST"), r"/api/provider-capacity/configuration", PLATFORM, "provider_capacity", "Provider capacity settings", True),
|
|
51
|
+
ConsoleRoute(("GET",), r"/api/github-rate-limit", PLATFORM, "provider_capacity", "Provider rate-limit diagnostics", True),
|
|
52
|
+
ConsoleRoute(("GET", "POST"), r"/api/configuration", PLATFORM, "server_settings", "Server settings", True),
|
|
53
|
+
ConsoleRoute(("GET",), r"/api/(?:process-metrics|usage)", PLATFORM, "platform_components", "Platform diagnostics", True),
|
|
54
|
+
ConsoleRoute(("GET",), r"/api/host-admin/diagnostics", HOST_ADMIN, "host_admin", "Bounded installation diagnostics", True),
|
|
55
|
+
ConsoleRoute(("GET",), r"/api/central-data/export", PLATFORM, "server_settings", "Central data export", True),
|
|
56
|
+
ConsoleRoute(("POST",), r"/api/central-data/(?:relocate(?:/browse)?|import)", PLATFORM, "server_settings", "Central data transfer", True),
|
|
57
|
+
ConsoleRoute(("GET", "POST"), r"/api/central-database/configuration", PLATFORM, "server_settings", "Central database maintenance settings", True),
|
|
58
|
+
ConsoleRoute(("GET",), r"/v1/operations/projects", PLATFORM, "operations", "Operations project listing"),
|
|
59
|
+
ConsoleRoute(("GET",), r"/api/prompt-history", PROJECT, "project_history", "Project run history"),
|
|
60
|
+
ConsoleRoute(("GET",), r"/api/prompt-history/[a-z0-9][a-z0-9-]{0,63}/(?:report|chat|details)", PROJECT, "project_history", "Project run detail"),
|
|
61
|
+
ConsoleRoute(("GET",), r"/api/telemetry/[0-9]{4}-[0-9]{2}-[0-9]{2}", PROJECT, "project_history", "Project telemetry detail"),
|
|
62
|
+
ConsoleRoute(("POST",), r"/api/execution-(?:dismiss|retry)", PROJECT, "project_execution", "Project execution action"),
|
|
63
|
+
ConsoleRoute(("POST",), r"/api/dashboard-translate", PROJECT, "project_console", "Project Console translation"),
|
|
64
|
+
ConsoleRoute(("GET",), r"/diagnostics/topology", TRANSPORT_INTERNAL, "transport", "Transport topology diagnostic"),
|
|
65
|
+
ConsoleRoute(("GET",), r"/(?:healthz|readyz)", TRANSPORT_INTERNAL, "transport", "Transport probe"),
|
|
66
|
+
ConsoleRoute(("POST",), r"/v1/projects/[A-Za-z0-9._-]+/submissions", TRANSPORT_INTERNAL, "transport", "Authenticated submission ingress"),
|
|
67
|
+
ConsoleRoute(("POST",), r"/v1/agent/(?:pair|register|heartbeat|attachment)", TRANSPORT_INTERNAL, "transport", "Agent transport control"),
|
|
68
|
+
ConsoleRoute(("POST",), r"/api/runtime-directory/open", HISTORICAL_UNREACHABLE, "historical_unreachable", "Retired checkout runtime action"),
|
|
69
|
+
ConsoleRoute(("POST",), r"/api/configuration/inbox-location(?:/browse)?", HISTORICAL_UNREACHABLE, "historical_unreachable", "Retired local Inbox-watcher configuration"),
|
|
70
|
+
ConsoleRoute(("GET", "POST"), r"/api/logs/(?:inbox|dashboard)", HISTORICAL_UNREACHABLE, "historical_unreachable", "Retired legacy component-log routes"),
|
|
71
|
+
ConsoleRoute(("GET", "POST"), rf"/api/components/{RETIRED_COMPONENT_ALIAS_ROUTE_PATTERN}/(?:details|restart)", HISTORICAL_UNREACHABLE, "historical_unreachable", "Retired legacy component authority"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def route_owners(method: str, path: str) -> tuple[ConsoleRoute, ...]:
|
|
76
|
+
return tuple(route for route in ROUTE_OWNERSHIP_MATRIX if route.matches(method, path))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def route_owner(method: str, path: str) -> ConsoleRoute | None:
|
|
80
|
+
matches = route_owners(method, path)
|
|
81
|
+
if len(matches) > 1:
|
|
82
|
+
raise RuntimeError(f"AMBIGUOUS_ROUTE_OWNERSHIP: {method} {path}")
|
|
83
|
+
return matches[0] if matches else None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Stable, read-only contracts for external Engineering Platform consumers.
|
|
2
|
+
|
|
3
|
+
The contract layer projects canonical Engineering Platform evidence. It is
|
|
4
|
+
deliberately downstream from lifecycle code and contains no action executor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .models import ActionAuditRecord, ActionPolicyDecision, AllowedAction, EvidenceReference
|
|
8
|
+
from .ep_consumer import (
|
|
9
|
+
AuthEnvelope,
|
|
10
|
+
ConsumerEnvelope,
|
|
11
|
+
ContractError,
|
|
12
|
+
ErrorCode,
|
|
13
|
+
ErrorEnvelope,
|
|
14
|
+
RequestEnvelope,
|
|
15
|
+
ResponseEnvelope,
|
|
16
|
+
deserialize_request,
|
|
17
|
+
serialize,
|
|
18
|
+
)
|
|
19
|
+
from .projection import evaluate_action, get_allowed_actions, get_run_context
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
"ActionAuditRecord",
|
|
23
|
+
"ActionPolicyDecision",
|
|
24
|
+
"AllowedAction",
|
|
25
|
+
"AuthEnvelope",
|
|
26
|
+
"ConsumerEnvelope",
|
|
27
|
+
"ContractError",
|
|
28
|
+
"ErrorCode",
|
|
29
|
+
"ErrorEnvelope",
|
|
30
|
+
"EvidenceReference",
|
|
31
|
+
"RequestEnvelope",
|
|
32
|
+
"ResponseEnvelope",
|
|
33
|
+
"deserialize_request",
|
|
34
|
+
"evaluate_action",
|
|
35
|
+
"get_allowed_actions",
|
|
36
|
+
"get_run_context",
|
|
37
|
+
"serialize",
|
|
38
|
+
)
|