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,3679 @@
|
|
|
1
|
+
"""Standalone Engineering Platform Server foundation.
|
|
2
|
+
|
|
3
|
+
This module intentionally owns no project, Agent transport, credential, or
|
|
4
|
+
execution authority. It is the installation-owned runtime boundary on which
|
|
5
|
+
those later capabilities can be composed.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from html import escape
|
|
13
|
+
import http.server
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import plistlib
|
|
19
|
+
import re
|
|
20
|
+
import select
|
|
21
|
+
import shlex
|
|
22
|
+
import shutil
|
|
23
|
+
import signal
|
|
24
|
+
import sqlite3
|
|
25
|
+
# The lifecycle starts this module with a fixed argv; no shell is used.
|
|
26
|
+
import subprocess # nosec B404
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from threading import Lock, RLock, Timer
|
|
30
|
+
from typing import Mapping, Protocol
|
|
31
|
+
from urllib.error import URLError
|
|
32
|
+
from urllib.request import urlopen
|
|
33
|
+
from urllib.parse import SplitResult, parse_qs, urlsplit
|
|
34
|
+
from uuid import uuid4
|
|
35
|
+
|
|
36
|
+
from . import agent_trust
|
|
37
|
+
from . import central_database
|
|
38
|
+
from . import central_data_transfer
|
|
39
|
+
from . import console_route_ownership
|
|
40
|
+
from . import console_presentation
|
|
41
|
+
from . import server_console_services
|
|
42
|
+
from . import dashboard_translation
|
|
43
|
+
from . import dependabot_producer
|
|
44
|
+
from . import external_producer_binding
|
|
45
|
+
from . import file_inbox
|
|
46
|
+
from . import host_admin
|
|
47
|
+
from . import installation_relocation
|
|
48
|
+
from . import local_repository_binding
|
|
49
|
+
from . import project_topology
|
|
50
|
+
from . import submission_service
|
|
51
|
+
from . import server_relay
|
|
52
|
+
from . import server_service
|
|
53
|
+
from . import storage
|
|
54
|
+
from . import managed_codex_runtime
|
|
55
|
+
from . import provider_readiness
|
|
56
|
+
from .platform_components import (
|
|
57
|
+
PLATFORM_COMPONENT_BY_ID,
|
|
58
|
+
PLATFORM_COMPONENT_IDS,
|
|
59
|
+
PLATFORM_COMPONENTS,
|
|
60
|
+
PLATFORM_COMPONENT_ROUTE_PATTERN,
|
|
61
|
+
RETIRED_COMPONENT_ALIAS_ROUTE_PATTERN,
|
|
62
|
+
)
|
|
63
|
+
from .component_logging import (
|
|
64
|
+
LOG_LEVELS_AT_OR_ABOVE,
|
|
65
|
+
MAX_COMPONENT_LOG_PAGE_SIZE,
|
|
66
|
+
VALID_LEVELS,
|
|
67
|
+
component_logger,
|
|
68
|
+
log_event,
|
|
69
|
+
)
|
|
70
|
+
from .ep_consumer_credentials import verifier
|
|
71
|
+
from .parity_context import ParityProjectStore, project_context
|
|
72
|
+
from .platform_version import EngineeringPlatformManifest
|
|
73
|
+
from .providers import (
|
|
74
|
+
GitHubProvider,
|
|
75
|
+
CodexCliProvider,
|
|
76
|
+
LaunchdProvider,
|
|
77
|
+
LaunchdRuntimeDetails,
|
|
78
|
+
MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT,
|
|
79
|
+
LocalProcessProvider,
|
|
80
|
+
default_engineering_platform_codex_cli_prefix,
|
|
81
|
+
)
|
|
82
|
+
from .resources import package_path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
SERVER_CONFIGURATION_FILENAME = "server.json"
|
|
86
|
+
SERVER_IDENTITY_FILENAME = "runtime-identity.json"
|
|
87
|
+
SERVER_RUNTIME_FILENAME = "runtime.json"
|
|
88
|
+
SERVER_DATABASE_FILENAME = "engineering.db"
|
|
89
|
+
SERVER_CONFIGURATION_VERSION = 2
|
|
90
|
+
# ADR-0026 defines the first standalone store as the canonical schema-40
|
|
91
|
+
# product definitions plus immutable control provenance. This server-owned
|
|
92
|
+
# bootstrap is deliberately separate from the retired predecessor migration
|
|
93
|
+
# machinery: it creates a clean installation only and never accepts a source
|
|
94
|
+
# database path.
|
|
95
|
+
SERVER_STORE_SCHEMA_VERSION = 53
|
|
96
|
+
SERVER_ENVIRONMENT_DATA_ROOT = "EP_SERVER_DATA_ROOT"
|
|
97
|
+
FILE_INBOX_DIRECTORY = "file-inbox"
|
|
98
|
+
HTTP_JSON_OPENAPI_PATH = "/openapi.json"
|
|
99
|
+
_CENTRAL_LOG_SORT_COLUMNS = {
|
|
100
|
+
"line": "id",
|
|
101
|
+
"timestamp": "created_at",
|
|
102
|
+
"level": "json_extract(payload, '$.level')",
|
|
103
|
+
"event": "json_extract(payload, '$.event')",
|
|
104
|
+
"runId": "COALESCE(json_extract(payload, '$.run_id'), '')",
|
|
105
|
+
"details": "COALESCE(json_extract(payload, '$.diagnostic'), '')",
|
|
106
|
+
}
|
|
107
|
+
_CHILDREN: dict[int, subprocess.Popen[object]] = {}
|
|
108
|
+
_SAFE_ATTACHMENT_FILENAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
|
|
109
|
+
_SAFE_REPORT_ID = re.compile(r"[a-z0-9][a-z0-9-]{0,63}")
|
|
110
|
+
_PROVIDER_LOGIN_LOCK = Lock()
|
|
111
|
+
_PROVIDER_INSTALL_LOCK = Lock()
|
|
112
|
+
_CODEX_RATE_LIMIT_CACHE: tuple[float, bytes] | None = None
|
|
113
|
+
_CODEX_RATE_LIMIT_CACHE_LOCK = Lock()
|
|
114
|
+
_CODEX_IDENTITY_CACHE: tuple[float, dict[str, str]] | None = None
|
|
115
|
+
_CODEX_IDENTITY_CACHE_LOCK = Lock()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _http_json_openapi_document() -> dict[str, object]:
|
|
119
|
+
"""Return the published contract for the canonical HTTP JSON ingress.
|
|
120
|
+
|
|
121
|
+
The document is intentionally local and versioned with the Server: it is
|
|
122
|
+
an interface description, not a separate dashboard-owned API surface.
|
|
123
|
+
"""
|
|
124
|
+
submission_properties: dict[str, object] = {
|
|
125
|
+
"repository_id": {"type": "string"},
|
|
126
|
+
"producer": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"required": ["id", "type"],
|
|
129
|
+
"properties": {
|
|
130
|
+
"id": {"type": "string"},
|
|
131
|
+
"type": {"type": "string"},
|
|
132
|
+
"version": {"type": "string"},
|
|
133
|
+
},
|
|
134
|
+
"additionalProperties": False,
|
|
135
|
+
},
|
|
136
|
+
"prompt": {"type": "string"},
|
|
137
|
+
"idempotency_key": {"type": "string"},
|
|
138
|
+
"correlation_id": {"type": "string"},
|
|
139
|
+
"mission_id": {"type": "string"},
|
|
140
|
+
"engineering_action_id": {"type": "string"},
|
|
141
|
+
"constraints": {"type": "object", "additionalProperties": True},
|
|
142
|
+
"transport_receipt_id": {"type": "string"},
|
|
143
|
+
"transport_received_at": {"type": "string", "format": "date-time"},
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
"openapi": "3.0.3",
|
|
147
|
+
"info": {
|
|
148
|
+
"title": "Engineering Platform HTTP JSON API",
|
|
149
|
+
"version": "1",
|
|
150
|
+
"description": "Canonical authenticated submission ingress and platform health.",
|
|
151
|
+
},
|
|
152
|
+
"paths": {
|
|
153
|
+
"/health": {
|
|
154
|
+
"get": {
|
|
155
|
+
"summary": "Read aggregated Engineering Platform component health",
|
|
156
|
+
"description": "Returns every canonical Platform Component. The aggregate is healthy only when every critical component is healthy.",
|
|
157
|
+
"responses": {
|
|
158
|
+
"200": {"description": "Critical Platform Components are healthy"},
|
|
159
|
+
"503": {"description": "One or more critical Platform Components are degraded"},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
"/healthz": {
|
|
164
|
+
"get": {
|
|
165
|
+
"summary": "Read server health and readiness",
|
|
166
|
+
"responses": {"200": {"description": "Server health"}},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
"/readyz": {
|
|
170
|
+
"get": {
|
|
171
|
+
"summary": "Read server readiness",
|
|
172
|
+
"responses": {"200": {"description": "Server readiness"}},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
"/v1/projects/{project_id}/submissions": {
|
|
176
|
+
"post": {
|
|
177
|
+
"summary": "Submit a canonical Engineering Platform request",
|
|
178
|
+
"security": [{"consumerBearer": []}],
|
|
179
|
+
"parameters": [
|
|
180
|
+
{
|
|
181
|
+
"name": "project_id", "in": "path", "required": True,
|
|
182
|
+
"schema": {"type": "string"},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "EP-Submission-Transport", "in": "header", "required": False,
|
|
186
|
+
"schema": {"type": "string", "default": "HTTP"},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
"requestBody": {
|
|
190
|
+
"required": True,
|
|
191
|
+
"content": {
|
|
192
|
+
"application/json": {
|
|
193
|
+
"schema": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"required": ["repository_id", "producer", "prompt"],
|
|
196
|
+
"properties": submission_properties,
|
|
197
|
+
"additionalProperties": False,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
"responses": {
|
|
203
|
+
"200": {"description": "Submission accepted or idempotently repeated"},
|
|
204
|
+
"400": {"description": "Malformed or invalid submission"},
|
|
205
|
+
"401": {"description": "Missing or invalid consumer credential"},
|
|
206
|
+
"404": {"description": "Unknown project"},
|
|
207
|
+
"409": {"description": "Project unavailable or idempotency conflict"},
|
|
208
|
+
"413": {"description": "Payload exceeds 128 KiB"},
|
|
209
|
+
"415": {"description": "Content type is not application/json"},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
"components": {
|
|
215
|
+
"securitySchemes": {
|
|
216
|
+
"consumerBearer": {"type": "http", "scheme": "bearer", "bearerFormat": "opaque"},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _attachment_content_disposition(filename: object) -> str:
|
|
223
|
+
"""Build a fail-closed attachment header from a bounded ASCII filename.
|
|
224
|
+
|
|
225
|
+
Route validation is not a response-header security boundary. This helper
|
|
226
|
+
rejects control characters and all non-allowlisted filenames before a
|
|
227
|
+
value reaches ``BaseHTTPRequestHandler.send_header``.
|
|
228
|
+
"""
|
|
229
|
+
if not isinstance(filename, str):
|
|
230
|
+
raise ValueError("attachment filename is invalid")
|
|
231
|
+
sanitized = filename.replace("\r", "").replace("\n", "")
|
|
232
|
+
if sanitized != filename or not _SAFE_ATTACHMENT_FILENAME.fullmatch(sanitized):
|
|
233
|
+
raise ValueError("attachment filename is invalid")
|
|
234
|
+
return f'attachment; filename="{sanitized}"'
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _report_content_disposition(report_id: object) -> str:
|
|
238
|
+
"""Compose the report filename only after independently validating its id."""
|
|
239
|
+
if not isinstance(report_id, str) or not _SAFE_REPORT_ID.fullmatch(report_id):
|
|
240
|
+
raise ValueError("report identifier is invalid")
|
|
241
|
+
return _attachment_content_disposition(f"engineering-report-{report_id}.md")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _execution_runtime_status() -> dict[str, str]:
|
|
245
|
+
"""Project installed Server Python readiness without Dashboard ownership."""
|
|
246
|
+
# ``sys.executable`` is the installed venv launcher. Do not resolve its
|
|
247
|
+
# symlink to the base interpreter: the Console must report the runtime
|
|
248
|
+
# that actually owns EP and its validation environment.
|
|
249
|
+
executable = Path(sys.executable).expanduser().absolute()
|
|
250
|
+
ready = executable.is_file() and os.access(executable, os.X_OK)
|
|
251
|
+
return {
|
|
252
|
+
"state": "READY" if ready else "UNAVAILABLE",
|
|
253
|
+
"executable": str(executable) if ready else "",
|
|
254
|
+
"version": sys.version.split()[0] if ready else "",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _remaining_rate_limit_capacity(rate_limits: dict[str, object]) -> float | None:
|
|
259
|
+
"""Return the most restrictive remaining safe quota percentage."""
|
|
260
|
+
windows = rate_limits.get("windows")
|
|
261
|
+
if not isinstance(windows, list):
|
|
262
|
+
return None
|
|
263
|
+
remaining: list[float] = []
|
|
264
|
+
for window in windows:
|
|
265
|
+
if not isinstance(window, dict):
|
|
266
|
+
continue
|
|
267
|
+
used = window.get("used_percent")
|
|
268
|
+
if isinstance(used, (int, float)) and not isinstance(used, bool):
|
|
269
|
+
remaining.append(max(0.0, min(100.0, 100.0 - float(used))))
|
|
270
|
+
return min(remaining) if remaining else None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _github_rate_limit_status() -> dict[str, object]:
|
|
274
|
+
"""Read GitHub quota state without changing GitHub or repository state."""
|
|
275
|
+
try:
|
|
276
|
+
payload = json.loads(GitHubProvider().github("api", "rate_limit"))
|
|
277
|
+
except (OSError, RuntimeError, json.JSONDecodeError) as error:
|
|
278
|
+
return {"limited": "rate limit" in str(error).lower()}
|
|
279
|
+
resources = payload.get("resources") if isinstance(payload, dict) else None
|
|
280
|
+
if not isinstance(resources, dict):
|
|
281
|
+
return {"limited": False}
|
|
282
|
+
exhausted: list[tuple[str, int]] = []
|
|
283
|
+
for name in ("core", "graphql", "search"):
|
|
284
|
+
resource = resources.get(name)
|
|
285
|
+
if not isinstance(resource, dict):
|
|
286
|
+
continue
|
|
287
|
+
remaining, reset = resource.get("remaining"), resource.get("reset")
|
|
288
|
+
if isinstance(remaining, int) and remaining <= 0:
|
|
289
|
+
exhausted.append((name, reset if isinstance(reset, int) else 0))
|
|
290
|
+
if not exhausted:
|
|
291
|
+
return {"limited": False}
|
|
292
|
+
reset_at = min((reset for _, reset in exhausted if reset > 0), default=None)
|
|
293
|
+
return {"limited": True, "reset_at": reset_at}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _codex_rate_limits() -> bytes:
|
|
297
|
+
"""Read quota through the Server-owned app-server protocol/cache."""
|
|
298
|
+
global _CODEX_RATE_LIMIT_CACHE
|
|
299
|
+
now = time.monotonic()
|
|
300
|
+
with _CODEX_RATE_LIMIT_CACHE_LOCK:
|
|
301
|
+
if _CODEX_RATE_LIMIT_CACHE and now - _CODEX_RATE_LIMIT_CACHE[0] < 60:
|
|
302
|
+
return _CODEX_RATE_LIMIT_CACHE[1]
|
|
303
|
+
identity = _codex_provider_identity()
|
|
304
|
+
provider = CodexCliProvider(); process = None
|
|
305
|
+
try:
|
|
306
|
+
process = provider.app_server()
|
|
307
|
+
if process.stdin is None or process.stdout is None:
|
|
308
|
+
return json.dumps(identity, separators=(",", ":")).encode()
|
|
309
|
+
process.stdin.write(json.dumps({"method": "initialize", "id": 1, "params": {"clientInfo": {"name": "engineering-platform-server", "title": "EP Operations", "version": _console_platform_version()}}}) + "\n")
|
|
310
|
+
process.stdin.flush(); deadline = time.monotonic() + 5; requested = False
|
|
311
|
+
while time.monotonic() < deadline:
|
|
312
|
+
ready, _, _ = select.select((process.stdout,), (), (), max(0, deadline - time.monotonic()))
|
|
313
|
+
if not ready: break
|
|
314
|
+
line = process.stdout.readline()
|
|
315
|
+
if not line: break
|
|
316
|
+
response = json.loads(line)
|
|
317
|
+
if response.get("id") == 1 and not requested:
|
|
318
|
+
process.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
|
|
319
|
+
process.stdin.write(json.dumps({"method": "account/rateLimits/read", "id": 2, "params": {}}) + "\n")
|
|
320
|
+
process.stdin.flush(); requested = True
|
|
321
|
+
elif response.get("id") == 2:
|
|
322
|
+
encoded = json.dumps({**identity, **_normalize_rate_limits(response.get("result"))}, separators=(",", ":")).encode()
|
|
323
|
+
with _CODEX_RATE_LIMIT_CACHE_LOCK: _CODEX_RATE_LIMIT_CACHE = (time.monotonic(), encoded)
|
|
324
|
+
return encoded
|
|
325
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
326
|
+
pass
|
|
327
|
+
finally:
|
|
328
|
+
if process is not None: provider.close_app_server(process)
|
|
329
|
+
return json.dumps(identity, separators=(",", ":")).encode()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _normalize_rate_limits(payload: object) -> dict[str, object]:
|
|
333
|
+
"""Keep only bounded display values from the read-only quota response."""
|
|
334
|
+
limits = payload.get("rateLimits") if isinstance(payload, dict) else None
|
|
335
|
+
if not isinstance(limits, dict): return {}
|
|
336
|
+
windows: list[dict[str, int | str]] = []
|
|
337
|
+
for key in ("primary", "secondary"):
|
|
338
|
+
item = limits.get(key)
|
|
339
|
+
if not isinstance(item, dict): continue
|
|
340
|
+
used, duration, resets = item.get("usedPercent"), item.get("windowDurationMins"), item.get("resetsAt")
|
|
341
|
+
if not isinstance(used, (int, float)) or isinstance(used, bool) or not isinstance(duration, int) or isinstance(duration, bool) or not isinstance(resets, int) or isinstance(resets, bool): continue
|
|
342
|
+
windows.append({"label": _rate_limit_window_label(duration), "used_percent": max(0, min(100, round(used))), "window_minutes": duration, "resets_at": resets})
|
|
343
|
+
credits = payload.get("rateLimitResetCredits"); available = credits.get("availableCount") if isinstance(credits, dict) else None
|
|
344
|
+
result: dict[str, object] = {"windows": windows}
|
|
345
|
+
if isinstance(available, int) and not isinstance(available, bool) and available >= 0: result["reset_credits"] = available
|
|
346
|
+
return result if windows or "reset_credits" in result else {}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _rate_limit_window_label(duration: int) -> str:
|
|
350
|
+
if duration == 300: return "5-uursvenster"
|
|
351
|
+
if duration == 10_080: return "Weekvenster"
|
|
352
|
+
if duration % 1_440 == 0: return f"{duration // 1_440}-daags venster"
|
|
353
|
+
if duration % 60 == 0: return f"{duration // 60}-uursvenster"
|
|
354
|
+
return f"{duration}-minutenvenster"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _codex_provider_identity() -> dict[str, str]:
|
|
358
|
+
"""Return the managed Codex CLI identity without selecting PATH authority."""
|
|
359
|
+
global _CODEX_IDENTITY_CACHE
|
|
360
|
+
now = time.monotonic()
|
|
361
|
+
with _CODEX_IDENTITY_CACHE_LOCK:
|
|
362
|
+
if _CODEX_IDENTITY_CACHE and now - _CODEX_IDENTITY_CACHE[0] < 300:
|
|
363
|
+
return dict(_CODEX_IDENTITY_CACHE[1])
|
|
364
|
+
identity = {"provider": "Codex CLI", "provider_version": "versie niet beschikbaar"}
|
|
365
|
+
executable = CodexCliProvider()._executable
|
|
366
|
+
if executable:
|
|
367
|
+
candidate = Path(executable).expanduser()
|
|
368
|
+
if candidate.is_absolute(): identity["provider_path"] = str(candidate.parent.parent)
|
|
369
|
+
try: completed = LocalProcessProvider().execute(default_data_root(), (executable, "--version"))
|
|
370
|
+
except OSError: completed = None
|
|
371
|
+
if completed and completed.returncode == 0:
|
|
372
|
+
match = re.search(r"(?<!\d)(\d+\.\d+\.\d+)(?!\d)", (completed.stdout or completed.stderr).strip())
|
|
373
|
+
if match: identity["provider_version"] = match.group(1)
|
|
374
|
+
with _CODEX_IDENTITY_CACHE_LOCK: _CODEX_IDENTITY_CACHE = (now, identity)
|
|
375
|
+
return dict(identity)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _start_provider_login(data_root: Path, provider: str) -> None:
|
|
379
|
+
"""Dispatch one explicit host-wide provider login from the Server."""
|
|
380
|
+
commands = {
|
|
381
|
+
"CODEX": (CodexCliProvider()._executable, "login", "--device-auth"),
|
|
382
|
+
"GITHUB": ("gh", "auth", "login", "--hostname", "github.com", "--web"),
|
|
383
|
+
}
|
|
384
|
+
command = commands.get(provider)
|
|
385
|
+
if command is None:
|
|
386
|
+
raise ValueError("Unsupported provider login request.")
|
|
387
|
+
if provider == "CODEX" and not CodexCliProvider().status().qualified:
|
|
388
|
+
raise ValueError("Codex CLI is not installed.")
|
|
389
|
+
if provider == "GITHUB" and shutil.which("gh") is None:
|
|
390
|
+
raise ValueError("GitHub CLI is not installed.")
|
|
391
|
+
if sys.platform != "darwin":
|
|
392
|
+
raise ValueError("Interactive provider login is supported from the local macOS Server only.")
|
|
393
|
+
script = "\n".join((
|
|
394
|
+
'tell application "Terminal"', "activate",
|
|
395
|
+
f"do script {json.dumps('exec ' + ' '.join(shlex.quote(part) for part in command))}",
|
|
396
|
+
"end tell",
|
|
397
|
+
))
|
|
398
|
+
with _PROVIDER_LOGIN_LOCK:
|
|
399
|
+
completed = LocalProcessProvider().execute(data_root, ("/usr/bin/osascript", "-e", script))
|
|
400
|
+
if completed.returncode:
|
|
401
|
+
raise ValueError("Provider login window could not be opened.")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _logout_provider(data_root: Path, provider: str) -> None:
|
|
405
|
+
"""Remove one locally stored provider session without exposing credentials."""
|
|
406
|
+
if provider == "CODEX":
|
|
407
|
+
completed = CodexCliProvider().command("logout")
|
|
408
|
+
elif provider == "GITHUB":
|
|
409
|
+
process = LocalProcessProvider()
|
|
410
|
+
account = process.execute(data_root, ("gh", "api", "user", "--jq", ".login"))
|
|
411
|
+
username = account.stdout.strip()
|
|
412
|
+
if account.returncode or not username or not re.fullmatch(r"[A-Za-z0-9-]+", username):
|
|
413
|
+
raise ValueError("GitHub session cannot be safely identified for logout.")
|
|
414
|
+
completed = process.execute(data_root, ("gh", "auth", "logout", "--hostname", "github.com", "--user", username))
|
|
415
|
+
else:
|
|
416
|
+
raise ValueError("Unsupported provider logout request.")
|
|
417
|
+
if completed.returncode:
|
|
418
|
+
raise ValueError("Provider logout did not complete.")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _central_execution_active(data_root: Path) -> bool:
|
|
422
|
+
"""Read active lifecycle state from CENTRAL, never a checkout status file."""
|
|
423
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
424
|
+
row = connection.execute(
|
|
425
|
+
"SELECT 1 FROM ep_parity_lifecycle_dispatches WHERE "
|
|
426
|
+
"state IN ('CLAIMED','RUNNING') OR (state IN ('BLOCKED','FAILED') "
|
|
427
|
+
"AND operator_resolution='OPEN') LIMIT 1"
|
|
428
|
+
).fetchone()
|
|
429
|
+
return row is not None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _choose_local_directory(data_root: Path) -> str | None:
|
|
433
|
+
"""Use the host-native folder chooser only after an explicit Console action."""
|
|
434
|
+
if sys.platform != "darwin":
|
|
435
|
+
raise ValueError("LOCAL_DIRECTORY_PICKER_UNAVAILABLE")
|
|
436
|
+
result = LocalProcessProvider().execute(data_root, ("osascript", "-e", "POSIX path of (choose folder)"))
|
|
437
|
+
if result.returncode:
|
|
438
|
+
if "-128" in (result.stderr or ""):
|
|
439
|
+
return None
|
|
440
|
+
raise ValueError("LOCAL_DIRECTORY_PICKER_FAILED")
|
|
441
|
+
location = result.stdout.strip()
|
|
442
|
+
if not location or not Path(location).is_dir():
|
|
443
|
+
raise ValueError("LOCAL_DIRECTORY_PICKER_FAILED")
|
|
444
|
+
return location
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _install_provider(data_root: Path, provider: str) -> None:
|
|
448
|
+
"""Install one provider only through the Server installation boundary."""
|
|
449
|
+
if not _PROVIDER_INSTALL_LOCK.acquire(blocking=False):
|
|
450
|
+
raise ValueError("Another provider installation is already in progress.")
|
|
451
|
+
try:
|
|
452
|
+
if _central_execution_active(data_root):
|
|
453
|
+
raise ValueError("Provider installation is unavailable while an execution is active.")
|
|
454
|
+
if provider == "CODEX":
|
|
455
|
+
try:
|
|
456
|
+
managed_codex_runtime.provision(data_root)
|
|
457
|
+
except managed_codex_runtime.ManagedCodexRuntimeError as error:
|
|
458
|
+
raise ValueError(str(error)) from error
|
|
459
|
+
key = "codex"
|
|
460
|
+
elif provider == "GITHUB":
|
|
461
|
+
brew = shutil.which("brew")
|
|
462
|
+
if brew is None:
|
|
463
|
+
raise ValueError("GitHub CLI installation requires Homebrew on this host.")
|
|
464
|
+
completed = LocalProcessProvider().execute(data_root, (brew, "install", "gh"))
|
|
465
|
+
verification = LocalProcessProvider().execute(data_root, ("gh", "--version"))
|
|
466
|
+
if completed.returncode or verification.returncode:
|
|
467
|
+
raise ValueError("Provider installation could not be verified.")
|
|
468
|
+
key = "github"
|
|
469
|
+
else:
|
|
470
|
+
raise ValueError("Unsupported provider installation request.")
|
|
471
|
+
if _central_provider_readiness(data_root).get(key, {}).get("state") == "UNAVAILABLE":
|
|
472
|
+
raise ValueError("Provider installation could not be verified.")
|
|
473
|
+
finally:
|
|
474
|
+
_PROVIDER_INSTALL_LOCK.release()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class ServerConfigurationError(ValueError):
|
|
478
|
+
"""Raised when an installation-owned server configuration is invalid."""
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
SERVER_REQUIRED_TABLES = frozenset(
|
|
482
|
+
{
|
|
483
|
+
"engineering_schema_migrations",
|
|
484
|
+
"engineering_metadata",
|
|
485
|
+
"ep_installations",
|
|
486
|
+
"ep_control_provenance",
|
|
487
|
+
"ep_consumer_credentials",
|
|
488
|
+
"ep_consumer_registrations",
|
|
489
|
+
"ep_project_registrations",
|
|
490
|
+
"ep_execution_runs",
|
|
491
|
+
"ep_execution_leases",
|
|
492
|
+
"prompt_execution_history",
|
|
493
|
+
"ep_agent_registrations",
|
|
494
|
+
"ep_agent_pairing_codes",
|
|
495
|
+
"ep_repository_registrations",
|
|
496
|
+
"ep_agent_repository_attachments",
|
|
497
|
+
"ep_local_repository_bindings",
|
|
498
|
+
"ep_submissions",
|
|
499
|
+
"ep_submission_events",
|
|
500
|
+
"ep_submission_prompt_history",
|
|
501
|
+
"ep_parity_lifecycle_dispatches",
|
|
502
|
+
"ep_receipt_run_provenance",
|
|
503
|
+
"ep_external_producer_bindings",
|
|
504
|
+
"ep_external_producer_binding_audit",
|
|
505
|
+
"engineering_transactions",
|
|
506
|
+
"execution_lifecycle_events",
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
SERVER_REQUIRED_INDEXES = frozenset(
|
|
510
|
+
{
|
|
511
|
+
"ep_consumer_credentials_scope_lookup",
|
|
512
|
+
"ep_consumer_registrations_status_lookup",
|
|
513
|
+
"ep_project_registrations_status_lookup",
|
|
514
|
+
"ep_execution_runs_project_lookup",
|
|
515
|
+
"ep_control_provenance_subject_lookup",
|
|
516
|
+
"ep_repository_registrations_project_lookup",
|
|
517
|
+
"ep_agent_repository_attachments_repository_lookup",
|
|
518
|
+
"ep_local_repository_bindings_repository_lookup",
|
|
519
|
+
"ep_submissions_project_lookup",
|
|
520
|
+
"ep_submissions_idempotency_lookup",
|
|
521
|
+
"ep_parity_lifecycle_dispatches_run_lookup",
|
|
522
|
+
"ep_receipt_run_provenance_project_lookup",
|
|
523
|
+
"ep_external_producer_bindings_active_key",
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@dataclass(frozen=True)
|
|
529
|
+
class ServerConfiguration:
|
|
530
|
+
version: int
|
|
531
|
+
bind_host: str
|
|
532
|
+
bind_port: int
|
|
533
|
+
managed_codex_cli_prefix: str
|
|
534
|
+
|
|
535
|
+
@classmethod
|
|
536
|
+
def load(cls, data_root: Path) -> "ServerConfiguration":
|
|
537
|
+
path = data_root / SERVER_CONFIGURATION_FILENAME
|
|
538
|
+
try:
|
|
539
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
540
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
541
|
+
raise ServerConfigurationError("EP Server configuration is unavailable.") from error
|
|
542
|
+
if not isinstance(raw, dict):
|
|
543
|
+
raise ServerConfigurationError("EP Server configuration is invalid.")
|
|
544
|
+
legacy_keys = {"version", "bind_host", "bind_port"}
|
|
545
|
+
current_keys = legacy_keys | {"managed_codex_cli_prefix"}
|
|
546
|
+
if set(raw) == legacy_keys and raw.get("version") == 1:
|
|
547
|
+
prefix = str(default_engineering_platform_codex_cli_prefix())
|
|
548
|
+
elif set(raw) == current_keys and raw.get("version") == SERVER_CONFIGURATION_VERSION:
|
|
549
|
+
prefix = raw.get("managed_codex_cli_prefix")
|
|
550
|
+
else:
|
|
551
|
+
raise ServerConfigurationError("EP Server configuration is invalid.")
|
|
552
|
+
candidate = Path(prefix).expanduser() if isinstance(prefix, str) else None
|
|
553
|
+
if (
|
|
554
|
+
not isinstance(raw["bind_host"], str)
|
|
555
|
+
or raw["bind_host"] != "127.0.0.1"
|
|
556
|
+
or not isinstance(raw["bind_port"], int)
|
|
557
|
+
or not 1 <= raw["bind_port"] <= 65535
|
|
558
|
+
or candidate is None
|
|
559
|
+
or not candidate.is_absolute()
|
|
560
|
+
):
|
|
561
|
+
raise ServerConfigurationError("EP Server configuration is invalid.")
|
|
562
|
+
return cls(int(raw["version"]), raw["bind_host"], raw["bind_port"], str(candidate.resolve(strict=False)))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclass(frozen=True)
|
|
566
|
+
class RuntimeIdentity:
|
|
567
|
+
instance_id: str
|
|
568
|
+
created_at: str
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@dataclass(frozen=True)
|
|
572
|
+
class AgentRegistrationRequest:
|
|
573
|
+
"""Transport-neutral future Agent registration input.
|
|
574
|
+
|
|
575
|
+
B3 deliberately does not define authentication, enrollment persistence,
|
|
576
|
+
project attachment, or any network representation for this request.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
agent_id: str
|
|
580
|
+
agent_kind: str
|
|
581
|
+
capabilities: tuple[str, ...]
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class AgentRegistrationIntake(Protocol):
|
|
585
|
+
"""Future internal extension point; no transport/auth contract is implied."""
|
|
586
|
+
|
|
587
|
+
def accept(self, request: AgentRegistrationRequest) -> None: ...
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def default_data_root() -> Path:
|
|
591
|
+
override = os.environ.get(SERVER_ENVIRONMENT_DATA_ROOT)
|
|
592
|
+
if override:
|
|
593
|
+
return Path(override).expanduser().resolve()
|
|
594
|
+
if sys.platform == "darwin":
|
|
595
|
+
return Path.home() / "Library" / "Application Support" / "Engineering Platform Server"
|
|
596
|
+
if os.name == "nt":
|
|
597
|
+
base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
|
|
598
|
+
return (Path(base) if base else Path.home() / "AppData" / "Local") / "Engineering Platform Server"
|
|
599
|
+
return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "engineering-platform-server"
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _utcnow() -> str:
|
|
603
|
+
return datetime.now(timezone.utc).isoformat()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _write_json(path: Path, payload: object) -> None:
|
|
607
|
+
path.write_text(json.dumps(payload, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
608
|
+
path.chmod(0o600)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _table_names(connection: sqlite3.Connection) -> set[str]:
|
|
612
|
+
return {str(row[0]) for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _index_names(connection: sqlite3.Connection) -> set[str]:
|
|
616
|
+
return {str(row[0]) for row in connection.execute("SELECT name FROM sqlite_master WHERE type='index'")}
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _schema_version(connection: sqlite3.Connection) -> int:
|
|
620
|
+
if "engineering_schema_migrations" not in _table_names(connection):
|
|
621
|
+
return 0
|
|
622
|
+
row = connection.execute("SELECT MAX(version) FROM engineering_schema_migrations").fetchone()
|
|
623
|
+
return int(row[0]) if row and row[0] is not None else 0
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _install_schema_41(connection: sqlite3.Connection, identity: RuntimeIdentity) -> None:
|
|
627
|
+
"""Install the clean standalone schema and immutable control provenance."""
|
|
628
|
+
for statement in (
|
|
629
|
+
"CREATE TABLE IF NOT EXISTS engineering_schema_migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)",
|
|
630
|
+
"CREATE TABLE IF NOT EXISTS engineering_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
|
|
631
|
+
"CREATE TABLE IF NOT EXISTS ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version=41))",
|
|
632
|
+
"CREATE TABLE IF NOT EXISTS ep_control_provenance (event_id INTEGER PRIMARY KEY, event_kind TEXT NOT NULL CHECK(event_kind IN ('INSTALLATION_CREATED','CREDENTIAL_LIFECYCLE','CONSUMER_REGISTRATION','PROJECT_SCOPE_MUTATION')), subject_kind TEXT NOT NULL, subject_id TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)",
|
|
633
|
+
"CREATE INDEX IF NOT EXISTS ep_control_provenance_subject_lookup ON ep_control_provenance(subject_kind,subject_id,event_id DESC)",
|
|
634
|
+
"CREATE TABLE IF NOT EXISTS ep_consumer_credentials (credential_id TEXT PRIMARY KEY CHECK(length(credential_id) BETWEEN 1 AND 128), consumer_id TEXT NOT NULL CHECK(length(consumer_id) BETWEEN 1 AND 128), project_id TEXT NOT NULL CHECK(length(project_id) BETWEEN 1 AND 128), verifier BLOB NOT NULL UNIQUE CHECK(length(verifier)=32), fingerprint BLOB NOT NULL UNIQUE CHECK(length(fingerprint)=32), issued_at TEXT NOT NULL, expires_at TEXT, revoked_at TEXT, replaced_by_credential_id TEXT REFERENCES ep_consumer_credentials(credential_id))",
|
|
635
|
+
"CREATE INDEX IF NOT EXISTS ep_consumer_credentials_scope_lookup ON ep_consumer_credentials(consumer_id,project_id,revoked_at)",
|
|
636
|
+
"CREATE TABLE IF NOT EXISTS ep_consumer_registrations (consumer_id TEXT NOT NULL CHECK(length(consumer_id) BETWEEN 1 AND 128), project_id TEXT NOT NULL CHECK(length(project_id) BETWEEN 1 AND 128), status TEXT NOT NULL CHECK(status IN ('ACTIVE','DISABLED','REVOKED')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, disabled_at TEXT, revoked_at TEXT, audit_metadata TEXT NOT NULL DEFAULT '{}', PRIMARY KEY(consumer_id,project_id))",
|
|
637
|
+
"CREATE INDEX IF NOT EXISTS ep_consumer_registrations_status_lookup ON ep_consumer_registrations(consumer_id,project_id,status)",
|
|
638
|
+
"CREATE TABLE IF NOT EXISTS ep_project_registrations (project_id TEXT PRIMARY KEY CHECK(length(project_id) BETWEEN 1 AND 128), attachment_contract TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('ACTIVE','DISABLED','REVOKED')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
|
639
|
+
"CREATE INDEX IF NOT EXISTS ep_project_registrations_status_lookup ON ep_project_registrations(status,project_id)",
|
|
640
|
+
"CREATE TABLE IF NOT EXISTS ep_execution_runs (run_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id), state TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
|
641
|
+
"CREATE INDEX IF NOT EXISTS ep_execution_runs_project_lookup ON ep_execution_runs(project_id,state,created_at DESC)",
|
|
642
|
+
"CREATE TABLE IF NOT EXISTS ep_execution_leases (lease_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES ep_execution_runs(run_id), holder_id TEXT NOT NULL, acquired_at TEXT NOT NULL, expires_at TEXT NOT NULL, released_at TEXT)",
|
|
643
|
+
"CREATE TABLE IF NOT EXISTS prompt_execution_history (run_id TEXT PRIMARY KEY REFERENCES ep_execution_runs(run_id), prompt_digest TEXT NOT NULL, recorded_at TEXT NOT NULL)",
|
|
644
|
+
):
|
|
645
|
+
connection.execute(statement)
|
|
646
|
+
agent_trust.install_schema(connection)
|
|
647
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(41)")
|
|
648
|
+
connection.execute("INSERT OR IGNORE INTO engineering_metadata(key,value) VALUES('installation.instance_id',?)", (identity.instance_id,))
|
|
649
|
+
connection.execute("INSERT OR IGNORE INTO engineering_metadata(key,value) VALUES('installation.schema_version','41')")
|
|
650
|
+
connection.execute("INSERT OR IGNORE INTO ep_installations(instance_id,created_at,schema_version) VALUES(?,?,41)", (identity.instance_id, identity.created_at))
|
|
651
|
+
connection.execute("INSERT OR IGNORE INTO ep_control_provenance(event_kind,subject_kind,subject_id,payload,recorded_at) VALUES('INSTALLATION_CREATED','installation',?,?,?)", (identity.instance_id, json.dumps({'schema_version': 41}, sort_keys=True), identity.created_at))
|
|
652
|
+
for table in ("ep_control_provenance",):
|
|
653
|
+
for operation in ("UPDATE", "DELETE"):
|
|
654
|
+
connection.execute(f"CREATE TRIGGER IF NOT EXISTS {table}_immutable_{operation.casefold()} BEFORE {operation} ON {table} BEGIN SELECT RAISE(ABORT, '{table} evidence is immutable.'); END")
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _migrate_schema_42(connection: sqlite3.Connection) -> None:
|
|
658
|
+
"""Forward-only topology extension; schema-41 structures remain intact."""
|
|
659
|
+
# Schema 41 deliberately constrained the bootstrap record to 41. Preserve
|
|
660
|
+
# its row while widening that bootstrap-only constraint for official
|
|
661
|
+
# forward migrations; no operational rows are rewritten.
|
|
662
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema41")
|
|
663
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42)))")
|
|
664
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,42 FROM ep_installations_schema41")
|
|
665
|
+
connection.execute("DROP TABLE ep_installations_schema41")
|
|
666
|
+
project_topology.install_schema(connection)
|
|
667
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(42)")
|
|
668
|
+
connection.execute("UPDATE engineering_metadata SET value='42' WHERE key='installation.schema_version'")
|
|
669
|
+
connection.execute("UPDATE ep_installations SET schema_version=42")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _migrate_schema_43(connection: sqlite3.Connection) -> None:
|
|
673
|
+
"""Add CENTRAL-owned canonical submission persistence.
|
|
674
|
+
|
|
675
|
+
This is deliberately a forward migration from schema 42; historical
|
|
676
|
+
schema-40 execution databases are neither inspected nor imported.
|
|
677
|
+
"""
|
|
678
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema42")
|
|
679
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43)))")
|
|
680
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,43 FROM ep_installations_schema42")
|
|
681
|
+
connection.execute("DROP TABLE ep_installations_schema42")
|
|
682
|
+
connection.execute("""CREATE TABLE ep_submissions (
|
|
683
|
+
submission_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
684
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
685
|
+
producer_id TEXT NOT NULL, producer_type TEXT NOT NULL, producer_version TEXT,
|
|
686
|
+
transport TEXT NOT NULL CHECK(transport IN ('HTTP','CLI','FILE_INBOX','LEGACY_FILE')),
|
|
687
|
+
prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL,
|
|
688
|
+
idempotency_key TEXT, correlation_id TEXT, mission_id TEXT, engineering_action_id TEXT,
|
|
689
|
+
state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED')), admission TEXT NOT NULL,
|
|
690
|
+
created_at TEXT NOT NULL)""")
|
|
691
|
+
connection.execute("CREATE INDEX ep_submissions_project_lookup ON ep_submissions(project_id,state,created_at DESC)")
|
|
692
|
+
connection.execute("CREATE UNIQUE INDEX ep_submissions_idempotency_lookup ON ep_submissions(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL")
|
|
693
|
+
connection.execute("CREATE TABLE ep_submission_events (event_id INTEGER PRIMARY KEY, submission_id TEXT NOT NULL REFERENCES ep_submissions(submission_id), event_kind TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)")
|
|
694
|
+
connection.execute("CREATE TABLE ep_submission_prompt_history (submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id), prompt_digest TEXT NOT NULL, recorded_at TEXT NOT NULL)")
|
|
695
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(43)")
|
|
696
|
+
connection.execute("UPDATE engineering_metadata SET value='43' WHERE key='installation.schema_version'")
|
|
697
|
+
connection.execute("UPDATE ep_installations SET schema_version=43")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _migrate_schema_44(connection: sqlite3.Connection) -> None:
|
|
701
|
+
"""Add the private, explicit Phase-P local checkout binding surface."""
|
|
702
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema43")
|
|
703
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44)))")
|
|
704
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,44 FROM ep_installations_schema43")
|
|
705
|
+
connection.execute("DROP TABLE ep_installations_schema43")
|
|
706
|
+
local_repository_binding.install_schema(connection)
|
|
707
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(44)")
|
|
708
|
+
connection.execute("UPDATE engineering_metadata SET value='44' WHERE key='installation.schema_version'")
|
|
709
|
+
connection.execute("UPDATE ep_installations SET schema_version=44")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _migrate_schema_45(connection: sqlite3.Connection) -> None:
|
|
713
|
+
"""Add the single-writer CENTRAL-to-historical lifecycle association."""
|
|
714
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema44")
|
|
715
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45)))")
|
|
716
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,45 FROM ep_installations_schema44")
|
|
717
|
+
connection.execute("DROP TABLE ep_installations_schema44")
|
|
718
|
+
connection.execute("""CREATE TABLE ep_parity_lifecycle_dispatches (
|
|
719
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id),
|
|
720
|
+
project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
721
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
722
|
+
run_id TEXT NOT NULL UNIQUE REFERENCES ep_execution_runs(run_id),
|
|
723
|
+
state TEXT NOT NULL CHECK(state IN ('CLAIMED','RUNNING','COMPLETE','BLOCKED','FAILED')),
|
|
724
|
+
prompt_path TEXT NOT NULL,
|
|
725
|
+
claimed_at TEXT NOT NULL,
|
|
726
|
+
updated_at TEXT NOT NULL
|
|
727
|
+
)""")
|
|
728
|
+
connection.execute("CREATE INDEX ep_parity_lifecycle_dispatches_run_lookup ON ep_parity_lifecycle_dispatches(run_id,state)")
|
|
729
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(45)")
|
|
730
|
+
connection.execute("UPDATE engineering_metadata SET value='45' WHERE key='installation.schema_version'")
|
|
731
|
+
connection.execute("UPDATE ep_installations SET schema_version=45")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _migrate_schema_46(connection: sqlite3.Connection) -> None:
|
|
735
|
+
"""Persist the admitted execution mode with the CENTRAL run.
|
|
736
|
+
|
|
737
|
+
The mode is decided before a submission is claimed. It is therefore run
|
|
738
|
+
evidence, rather than a presentation value to be rediscovered from a
|
|
739
|
+
mutable prompt or a repository-local telemetry row.
|
|
740
|
+
"""
|
|
741
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema45")
|
|
742
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46)))")
|
|
743
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,46 FROM ep_installations_schema45")
|
|
744
|
+
connection.execute("DROP TABLE ep_installations_schema45")
|
|
745
|
+
# Existing CENTRAL runs predate this evidence field. Keep them NULL so
|
|
746
|
+
# the Console accurately reports that their mode was not recorded, rather
|
|
747
|
+
# than silently inventing MANAGED during migration.
|
|
748
|
+
connection.execute("ALTER TABLE ep_execution_runs ADD COLUMN execution_mode TEXT CHECK(execution_mode IN ('MANAGED','GENESIS'))")
|
|
749
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(46)")
|
|
750
|
+
connection.execute("UPDATE engineering_metadata SET value='46' WHERE key='installation.schema_version'")
|
|
751
|
+
connection.execute("UPDATE ep_installations SET schema_version=46")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _migrate_schema_47(connection: sqlite3.Connection) -> None:
|
|
755
|
+
"""Keep failed project runs FIFO-blocking until CENTRAL records a resolution."""
|
|
756
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema46")
|
|
757
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47)))")
|
|
758
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,47 FROM ep_installations_schema46")
|
|
759
|
+
connection.execute("DROP TABLE ep_installations_schema46")
|
|
760
|
+
connection.execute("ALTER TABLE ep_parity_lifecycle_dispatches ADD COLUMN operator_resolution TEXT NOT NULL DEFAULT 'NONE' CHECK(operator_resolution IN ('NONE','OPEN','DISMISSED','RETRIED'))")
|
|
761
|
+
connection.execute("ALTER TABLE ep_parity_lifecycle_dispatches ADD COLUMN resolution_submission_id TEXT REFERENCES ep_submissions(submission_id)")
|
|
762
|
+
connection.execute("UPDATE ep_parity_lifecycle_dispatches SET operator_resolution='OPEN' WHERE state IN ('BLOCKED','FAILED')")
|
|
763
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(47)")
|
|
764
|
+
connection.execute("UPDATE engineering_metadata SET value='47' WHERE key='installation.schema_version'")
|
|
765
|
+
connection.execute("UPDATE ep_installations SET schema_version=47")
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _migrate_schema_48(connection: sqlite3.Connection) -> None:
|
|
769
|
+
"""Move retained lifecycle persistence into the one CENTRAL database.
|
|
770
|
+
|
|
771
|
+
No repository is opened or scanned. These are empty compatibility tables
|
|
772
|
+
for new standalone runs while the preserved runner is being invoked via
|
|
773
|
+
the explicit CENTRAL operational context.
|
|
774
|
+
"""
|
|
775
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema47")
|
|
776
|
+
connection.execute(
|
|
777
|
+
"CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, "
|
|
778
|
+
"schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48)))"
|
|
779
|
+
)
|
|
780
|
+
connection.execute(
|
|
781
|
+
"INSERT INTO ep_installations(instance_id,created_at,schema_version) "
|
|
782
|
+
"SELECT instance_id,created_at,48 FROM ep_installations_schema47"
|
|
783
|
+
)
|
|
784
|
+
connection.execute("DROP TABLE ep_installations_schema47")
|
|
785
|
+
storage.install_central_operational_compatibility_schema(connection)
|
|
786
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(48)")
|
|
787
|
+
connection.execute("UPDATE engineering_metadata SET value='48' WHERE key='installation.schema_version'")
|
|
788
|
+
connection.execute("UPDATE ep_installations SET schema_version=48")
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _migrate_schema_49(connection: sqlite3.Connection) -> None:
|
|
792
|
+
"""Record bounded ingress receipts in the canonical submission row."""
|
|
793
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema48")
|
|
794
|
+
connection.execute(
|
|
795
|
+
"CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, "
|
|
796
|
+
"schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49)))"
|
|
797
|
+
)
|
|
798
|
+
connection.execute(
|
|
799
|
+
"INSERT INTO ep_installations(instance_id,created_at,schema_version) "
|
|
800
|
+
"SELECT instance_id,created_at,49 FROM ep_installations_schema48"
|
|
801
|
+
)
|
|
802
|
+
connection.execute("DROP TABLE ep_installations_schema48")
|
|
803
|
+
# SQLite cannot widen the schema-43 transport CHECK in place. Rebuild the
|
|
804
|
+
# parent table while foreign-key enforcement is temporarily disabled by
|
|
805
|
+
# the caller; SQLite keeps dependent references pointed at its canonical
|
|
806
|
+
# name. No submission facts are rewritten or inferred.
|
|
807
|
+
# Child tables must be rebuilt too: SQLite otherwise retains a foreign-key
|
|
808
|
+
# reference to the renamed historical parent table.
|
|
809
|
+
connection.execute("ALTER TABLE ep_submission_events RENAME TO ep_submission_events_schema48")
|
|
810
|
+
connection.execute("ALTER TABLE ep_submission_prompt_history RENAME TO ep_submission_prompt_history_schema48")
|
|
811
|
+
connection.execute("ALTER TABLE ep_parity_lifecycle_dispatches RENAME TO ep_parity_lifecycle_dispatches_schema48")
|
|
812
|
+
connection.execute("ALTER TABLE ep_submissions RENAME TO ep_submissions_schema48")
|
|
813
|
+
connection.execute("""CREATE TABLE ep_submissions (
|
|
814
|
+
submission_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
815
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
816
|
+
producer_id TEXT NOT NULL, producer_type TEXT NOT NULL, producer_version TEXT,
|
|
817
|
+
transport TEXT NOT NULL CHECK(transport IN ('HTTP','CLI','FILE_INBOX','LEGACY_FILE')),
|
|
818
|
+
prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL,
|
|
819
|
+
idempotency_key TEXT, correlation_id TEXT, mission_id TEXT, engineering_action_id TEXT,
|
|
820
|
+
transport_receipt_id TEXT, transport_received_at TEXT,
|
|
821
|
+
state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED')), admission TEXT NOT NULL,
|
|
822
|
+
created_at TEXT NOT NULL)""")
|
|
823
|
+
connection.execute("""INSERT INTO ep_submissions(
|
|
824
|
+
submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,state,admission,created_at)
|
|
825
|
+
SELECT submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,state,admission,created_at
|
|
826
|
+
FROM ep_submissions_schema48""")
|
|
827
|
+
connection.execute("""CREATE TABLE ep_submission_events (
|
|
828
|
+
event_id INTEGER PRIMARY KEY, submission_id TEXT NOT NULL REFERENCES ep_submissions(submission_id),
|
|
829
|
+
event_kind TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)""")
|
|
830
|
+
connection.execute("""INSERT INTO ep_submission_events(event_id,submission_id,event_kind,payload,recorded_at)
|
|
831
|
+
SELECT event_id,submission_id,event_kind,payload,recorded_at FROM ep_submission_events_schema48""")
|
|
832
|
+
connection.execute("""CREATE TABLE ep_submission_prompt_history (
|
|
833
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id), prompt_digest TEXT NOT NULL,
|
|
834
|
+
recorded_at TEXT NOT NULL)""")
|
|
835
|
+
connection.execute("""INSERT INTO ep_submission_prompt_history(submission_id,prompt_digest,recorded_at)
|
|
836
|
+
SELECT submission_id,prompt_digest,recorded_at FROM ep_submission_prompt_history_schema48""")
|
|
837
|
+
connection.execute("""CREATE TABLE ep_parity_lifecycle_dispatches (
|
|
838
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id),
|
|
839
|
+
project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
840
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
841
|
+
run_id TEXT NOT NULL UNIQUE REFERENCES ep_execution_runs(run_id),
|
|
842
|
+
state TEXT NOT NULL CHECK(state IN ('CLAIMED','RUNNING','COMPLETE','BLOCKED','FAILED')),
|
|
843
|
+
prompt_path TEXT NOT NULL, claimed_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
844
|
+
operator_resolution TEXT NOT NULL DEFAULT 'NONE' CHECK(operator_resolution IN ('NONE','OPEN','DISMISSED','RETRIED')),
|
|
845
|
+
resolution_submission_id TEXT REFERENCES ep_submissions(submission_id))""")
|
|
846
|
+
connection.execute("""INSERT INTO ep_parity_lifecycle_dispatches(
|
|
847
|
+
submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution,resolution_submission_id)
|
|
848
|
+
SELECT submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution,resolution_submission_id
|
|
849
|
+
FROM ep_parity_lifecycle_dispatches_schema48""")
|
|
850
|
+
connection.execute("DROP TABLE ep_submission_events_schema48")
|
|
851
|
+
connection.execute("DROP TABLE ep_submission_prompt_history_schema48")
|
|
852
|
+
connection.execute("DROP TABLE ep_parity_lifecycle_dispatches_schema48")
|
|
853
|
+
connection.execute("DROP TABLE ep_submissions_schema48")
|
|
854
|
+
connection.execute("CREATE INDEX ep_submissions_project_lookup ON ep_submissions(project_id,state,created_at DESC)")
|
|
855
|
+
connection.execute("CREATE UNIQUE INDEX ep_submissions_idempotency_lookup ON ep_submissions(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL")
|
|
856
|
+
connection.execute("CREATE INDEX ep_parity_lifecycle_dispatches_run_lookup ON ep_parity_lifecycle_dispatches(run_id,state)")
|
|
857
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(49)")
|
|
858
|
+
connection.execute("UPDATE engineering_metadata SET value='49' WHERE key='installation.schema_version'")
|
|
859
|
+
connection.execute("UPDATE ep_installations SET schema_version=49")
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _migrate_schema_50(connection: sqlite3.Connection) -> None:
|
|
863
|
+
"""Add CENTRAL-owned external producer bindings and immutable audit evidence."""
|
|
864
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema49")
|
|
865
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50)))")
|
|
866
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,50 FROM ep_installations_schema49")
|
|
867
|
+
connection.execute("DROP TABLE ep_installations_schema49")
|
|
868
|
+
connection.execute("""CREATE TABLE ep_external_producer_bindings (
|
|
869
|
+
binding_id TEXT PRIMARY KEY, producer_type TEXT NOT NULL, external_resource_type TEXT NOT NULL,
|
|
870
|
+
external_resource_identity TEXT NOT NULL, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
871
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id), status TEXT NOT NULL,
|
|
872
|
+
version INTEGER NOT NULL, created_at TEXT NOT NULL, created_by TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
873
|
+
provenance TEXT NOT NULL)""")
|
|
874
|
+
connection.execute("CREATE UNIQUE INDEX ep_external_producer_bindings_active_key ON ep_external_producer_bindings(producer_type,external_resource_type,external_resource_identity) WHERE status='ACTIVE'")
|
|
875
|
+
connection.execute("""CREATE TABLE ep_external_producer_binding_audit (
|
|
876
|
+
audit_id INTEGER PRIMARY KEY, binding_id TEXT NOT NULL, action TEXT NOT NULL, actor TEXT NOT NULL,
|
|
877
|
+
reason TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)""")
|
|
878
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(50)")
|
|
879
|
+
connection.execute("UPDATE engineering_metadata SET value='50' WHERE key='installation.schema_version'")
|
|
880
|
+
connection.execute("UPDATE ep_installations SET schema_version=50")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _migrate_schema_51(connection: sqlite3.Connection) -> None:
|
|
884
|
+
"""Add the explicit Server-owned Dependabot transport value.
|
|
885
|
+
|
|
886
|
+
The producer is a bounded internal adapter, not an HTTP caller and not a
|
|
887
|
+
File Inbox delivery. SQLite requires the durable submission constraint to
|
|
888
|
+
be rebuilt to record that distinction truthfully.
|
|
889
|
+
"""
|
|
890
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema50")
|
|
891
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51)))")
|
|
892
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,51 FROM ep_installations_schema50")
|
|
893
|
+
connection.execute("DROP TABLE ep_installations_schema50")
|
|
894
|
+
connection.execute("ALTER TABLE ep_submission_events RENAME TO ep_submission_events_schema50")
|
|
895
|
+
connection.execute("ALTER TABLE ep_submission_prompt_history RENAME TO ep_submission_prompt_history_schema50")
|
|
896
|
+
connection.execute("ALTER TABLE ep_parity_lifecycle_dispatches RENAME TO ep_parity_lifecycle_dispatches_schema50")
|
|
897
|
+
connection.execute("ALTER TABLE ep_submissions RENAME TO ep_submissions_schema50")
|
|
898
|
+
connection.execute("""CREATE TABLE ep_submissions (
|
|
899
|
+
submission_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
900
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
901
|
+
producer_id TEXT NOT NULL, producer_type TEXT NOT NULL, producer_version TEXT,
|
|
902
|
+
transport TEXT NOT NULL CHECK(transport IN ('HTTP','CLI','FILE_INBOX','DEPENDABOT','LEGACY_FILE')),
|
|
903
|
+
prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL,
|
|
904
|
+
idempotency_key TEXT, correlation_id TEXT, mission_id TEXT, engineering_action_id TEXT,
|
|
905
|
+
transport_receipt_id TEXT, transport_received_at TEXT,
|
|
906
|
+
state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED')), admission TEXT NOT NULL,
|
|
907
|
+
created_at TEXT NOT NULL)""")
|
|
908
|
+
connection.execute("""INSERT INTO ep_submissions(
|
|
909
|
+
submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,transport_receipt_id,transport_received_at,state,admission,created_at)
|
|
910
|
+
SELECT submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,transport_receipt_id,transport_received_at,state,admission,created_at
|
|
911
|
+
FROM ep_submissions_schema50""")
|
|
912
|
+
connection.execute("""CREATE TABLE ep_submission_events (
|
|
913
|
+
event_id INTEGER PRIMARY KEY, submission_id TEXT NOT NULL REFERENCES ep_submissions(submission_id),
|
|
914
|
+
event_kind TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)""")
|
|
915
|
+
connection.execute("""INSERT INTO ep_submission_events(event_id,submission_id,event_kind,payload,recorded_at)
|
|
916
|
+
SELECT event_id,submission_id,event_kind,payload,recorded_at FROM ep_submission_events_schema50""")
|
|
917
|
+
connection.execute("""CREATE TABLE ep_submission_prompt_history (
|
|
918
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id), prompt_digest TEXT NOT NULL,
|
|
919
|
+
recorded_at TEXT NOT NULL)""")
|
|
920
|
+
connection.execute("""INSERT INTO ep_submission_prompt_history(submission_id,prompt_digest,recorded_at)
|
|
921
|
+
SELECT submission_id,prompt_digest,recorded_at FROM ep_submission_prompt_history_schema50""")
|
|
922
|
+
connection.execute("""CREATE TABLE ep_parity_lifecycle_dispatches (
|
|
923
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id),
|
|
924
|
+
project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
925
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
926
|
+
run_id TEXT NOT NULL UNIQUE REFERENCES ep_execution_runs(run_id),
|
|
927
|
+
state TEXT NOT NULL CHECK(state IN ('CLAIMED','RUNNING','COMPLETE','BLOCKED','FAILED')),
|
|
928
|
+
prompt_path TEXT NOT NULL, claimed_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
929
|
+
operator_resolution TEXT NOT NULL DEFAULT 'NONE' CHECK(operator_resolution IN ('NONE','OPEN','DISMISSED','RETRIED')),
|
|
930
|
+
resolution_submission_id TEXT REFERENCES ep_submissions(submission_id))""")
|
|
931
|
+
connection.execute("""INSERT INTO ep_parity_lifecycle_dispatches(
|
|
932
|
+
submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution,resolution_submission_id)
|
|
933
|
+
SELECT submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution,resolution_submission_id
|
|
934
|
+
FROM ep_parity_lifecycle_dispatches_schema50""")
|
|
935
|
+
for table in ("ep_submission_events_schema50", "ep_submission_prompt_history_schema50", "ep_parity_lifecycle_dispatches_schema50", "ep_submissions_schema50"):
|
|
936
|
+
connection.execute(f"DROP TABLE {table}")
|
|
937
|
+
connection.execute("CREATE INDEX ep_submissions_project_lookup ON ep_submissions(project_id,state,created_at DESC)")
|
|
938
|
+
connection.execute("CREATE UNIQUE INDEX ep_submissions_idempotency_lookup ON ep_submissions(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL")
|
|
939
|
+
connection.execute("CREATE INDEX ep_parity_lifecycle_dispatches_run_lookup ON ep_parity_lifecycle_dispatches(run_id,state)")
|
|
940
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(51)")
|
|
941
|
+
connection.execute("UPDATE engineering_metadata SET value='51' WHERE key='installation.schema_version'")
|
|
942
|
+
connection.execute("UPDATE ep_installations SET schema_version=51")
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _migrate_schema_52(connection: sqlite3.Connection) -> None:
|
|
946
|
+
"""Add the sole immutable CENTRAL receipt-to-run provenance authority."""
|
|
947
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema51")
|
|
948
|
+
connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51,52)))")
|
|
949
|
+
connection.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,52 FROM ep_installations_schema51")
|
|
950
|
+
connection.execute("DROP TABLE ep_installations_schema51")
|
|
951
|
+
connection.execute("""CREATE TABLE ep_receipt_run_provenance (
|
|
952
|
+
submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id),
|
|
953
|
+
run_id TEXT NOT NULL UNIQUE REFERENCES ep_execution_runs(run_id),
|
|
954
|
+
project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
|
|
955
|
+
repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
|
|
956
|
+
installation_id TEXT NOT NULL REFERENCES ep_installations(instance_id),
|
|
957
|
+
created_at TEXT NOT NULL
|
|
958
|
+
)""")
|
|
959
|
+
connection.execute("CREATE INDEX ep_receipt_run_provenance_project_lookup ON ep_receipt_run_provenance(project_id,created_at DESC)")
|
|
960
|
+
connection.execute("""CREATE TRIGGER ep_receipt_run_provenance_scope_insert
|
|
961
|
+
BEFORE INSERT ON ep_receipt_run_provenance BEGIN
|
|
962
|
+
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM ep_submissions s WHERE s.submission_id=NEW.submission_id AND s.project_id=NEW.project_id AND s.repository_id=NEW.repository_id) THEN RAISE(ABORT,'PROVENANCE_SUBMISSION_SCOPE_MISMATCH') END;
|
|
963
|
+
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM ep_execution_runs r WHERE r.run_id=NEW.run_id AND r.project_id=NEW.project_id) THEN RAISE(ABORT,'PROVENANCE_RUN_PROJECT_MISMATCH') END;
|
|
964
|
+
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM ep_parity_lifecycle_dispatches d WHERE d.run_id=NEW.run_id AND d.submission_id=NEW.submission_id AND d.project_id=NEW.project_id AND d.repository_id=NEW.repository_id) THEN RAISE(ABORT,'PROVENANCE_DISPATCH_SCOPE_MISMATCH') END;
|
|
965
|
+
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM engineering_metadata WHERE key='installation.instance_id' AND value=NEW.installation_id) THEN RAISE(ABORT,'PROVENANCE_INSTALLATION_MISMATCH') END;
|
|
966
|
+
END""")
|
|
967
|
+
for operation in ("UPDATE", "DELETE"):
|
|
968
|
+
connection.execute(f"CREATE TRIGGER ep_receipt_run_provenance_immutable_{operation.casefold()} BEFORE {operation} ON ep_receipt_run_provenance BEGIN SELECT RAISE(ABORT,'PROVENANCE_IMMUTABLE'); END")
|
|
969
|
+
# Backfill only rows whose canonical dispatch already proves every scope.
|
|
970
|
+
connection.execute("""INSERT INTO ep_receipt_run_provenance(submission_id,run_id,project_id,repository_id,installation_id,created_at)
|
|
971
|
+
SELECT d.submission_id,d.run_id,d.project_id,d.repository_id,m.value,d.claimed_at
|
|
972
|
+
FROM ep_parity_lifecycle_dispatches d JOIN ep_submissions s ON s.submission_id=d.submission_id AND s.project_id=d.project_id AND s.repository_id=d.repository_id
|
|
973
|
+
JOIN ep_execution_runs r ON r.run_id=d.run_id AND r.project_id=d.project_id
|
|
974
|
+
JOIN engineering_metadata m ON m.key='installation.instance_id'""")
|
|
975
|
+
missing = connection.execute(
|
|
976
|
+
"""SELECT 1 FROM ep_parity_lifecycle_dispatches d
|
|
977
|
+
WHERE NOT EXISTS (
|
|
978
|
+
SELECT 1 FROM ep_receipt_run_provenance p
|
|
979
|
+
WHERE p.submission_id=d.submission_id AND p.run_id=d.run_id
|
|
980
|
+
AND p.project_id=d.project_id AND p.repository_id=d.repository_id
|
|
981
|
+
) LIMIT 1"""
|
|
982
|
+
).fetchone()
|
|
983
|
+
if missing is not None:
|
|
984
|
+
raise ServerConfigurationError("CENTRAL receipt-to-run provenance migration is incomplete.")
|
|
985
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(52)")
|
|
986
|
+
connection.execute("UPDATE engineering_metadata SET value='52' WHERE key='installation.schema_version'")
|
|
987
|
+
connection.execute("UPDATE ep_installations SET schema_version=52")
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
_CONSUMER_CREDENTIAL_COLUMNS = (
|
|
991
|
+
"credential_id", "consumer_id", "project_id", "verifier", "fingerprint",
|
|
992
|
+
"issued_at", "expires_at", "revoked_at", "replaced_by_credential_id",
|
|
993
|
+
)
|
|
994
|
+
_CONSUMER_REGISTRATION_COLUMNS = (
|
|
995
|
+
"consumer_id", "project_id", "status", "created_at", "updated_at",
|
|
996
|
+
"disabled_at", "revoked_at", "audit_metadata",
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _table_columns(connection: sqlite3.Connection, table: str) -> tuple[str, ...]:
|
|
1001
|
+
return tuple(str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})"))
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _require_consumer_table_shape(
|
|
1005
|
+
connection: sqlite3.Connection, *, credentials: str, registrations: str,
|
|
1006
|
+
) -> None:
|
|
1007
|
+
"""Validate only table metadata; never surface credential values."""
|
|
1008
|
+
|
|
1009
|
+
if _table_columns(connection, credentials) != _CONSUMER_CREDENTIAL_COLUMNS:
|
|
1010
|
+
raise ServerConfigurationError("EP consumer credential table shape is invalid.")
|
|
1011
|
+
if _table_columns(connection, registrations) != _CONSUMER_REGISTRATION_COLUMNS:
|
|
1012
|
+
raise ServerConfigurationError("EP consumer registration table shape is invalid.")
|
|
1013
|
+
credential_pk = tuple(
|
|
1014
|
+
str(row[1]) for row in connection.execute(f"PRAGMA table_info({credentials})") if int(row[5]) > 0
|
|
1015
|
+
)
|
|
1016
|
+
registration_pk = tuple(
|
|
1017
|
+
str(row[1]) for row in connection.execute(f"PRAGMA table_info({registrations})") if int(row[5]) > 0
|
|
1018
|
+
)
|
|
1019
|
+
if credential_pk != ("credential_id",) or registration_pk != ("consumer_id", "project_id"):
|
|
1020
|
+
raise ServerConfigurationError("EP consumer credential table identity is invalid.")
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _install_ep_consumer_schema(connection: sqlite3.Connection) -> None:
|
|
1024
|
+
connection.execute(
|
|
1025
|
+
"CREATE TABLE ep_consumer_credentials ("
|
|
1026
|
+
"credential_id TEXT PRIMARY KEY CHECK(length(credential_id) BETWEEN 1 AND 128),"
|
|
1027
|
+
"consumer_id TEXT NOT NULL CHECK(length(consumer_id) BETWEEN 1 AND 128),"
|
|
1028
|
+
"project_id TEXT NOT NULL CHECK(length(project_id) BETWEEN 1 AND 128),"
|
|
1029
|
+
"verifier BLOB NOT NULL UNIQUE CHECK(length(verifier)=32),"
|
|
1030
|
+
"fingerprint BLOB NOT NULL UNIQUE CHECK(length(fingerprint)=32),"
|
|
1031
|
+
"issued_at TEXT NOT NULL,expires_at TEXT,revoked_at TEXT,"
|
|
1032
|
+
"replaced_by_credential_id TEXT REFERENCES ep_consumer_credentials(credential_id))"
|
|
1033
|
+
)
|
|
1034
|
+
connection.execute(
|
|
1035
|
+
"CREATE INDEX ep_consumer_credentials_scope_lookup "
|
|
1036
|
+
"ON ep_consumer_credentials(consumer_id,project_id,revoked_at)"
|
|
1037
|
+
)
|
|
1038
|
+
connection.execute(
|
|
1039
|
+
"CREATE TABLE ep_consumer_registrations ("
|
|
1040
|
+
"consumer_id TEXT NOT NULL CHECK(length(consumer_id) BETWEEN 1 AND 128),"
|
|
1041
|
+
"project_id TEXT NOT NULL CHECK(length(project_id) BETWEEN 1 AND 128),"
|
|
1042
|
+
"status TEXT NOT NULL CHECK(status IN ('ACTIVE','DISABLED','REVOKED')),"
|
|
1043
|
+
"created_at TEXT NOT NULL,updated_at TEXT NOT NULL,disabled_at TEXT,revoked_at TEXT,"
|
|
1044
|
+
"audit_metadata TEXT NOT NULL DEFAULT '{}',PRIMARY KEY(consumer_id,project_id))"
|
|
1045
|
+
)
|
|
1046
|
+
connection.execute(
|
|
1047
|
+
"CREATE INDEX ep_consumer_registrations_status_lookup "
|
|
1048
|
+
"ON ep_consumer_registrations(consumer_id,project_id,status)"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _assert_exact_consumer_transfer(
|
|
1053
|
+
connection: sqlite3.Connection, *, source: str, destination: str, columns: tuple[str, ...],
|
|
1054
|
+
) -> None:
|
|
1055
|
+
"""Prove cardinality and values match without exposing credential material."""
|
|
1056
|
+
|
|
1057
|
+
column_list = ",".join(columns)
|
|
1058
|
+
source_count = int(connection.execute(f"SELECT COUNT(*) FROM {source}").fetchone()[0])
|
|
1059
|
+
destination_count = int(connection.execute(f"SELECT COUNT(*) FROM {destination}").fetchone()[0])
|
|
1060
|
+
if source_count != destination_count:
|
|
1061
|
+
raise ServerConfigurationError("EP consumer credential transfer cardinality is invalid.")
|
|
1062
|
+
missing = connection.execute(
|
|
1063
|
+
f"SELECT {column_list} FROM {source} EXCEPT SELECT {column_list} FROM {destination} LIMIT 1"
|
|
1064
|
+
).fetchone()
|
|
1065
|
+
extra = connection.execute(
|
|
1066
|
+
f"SELECT {column_list} FROM {destination} EXCEPT SELECT {column_list} FROM {source} LIMIT 1"
|
|
1067
|
+
).fetchone()
|
|
1068
|
+
if missing is not None or extra is not None:
|
|
1069
|
+
raise ServerConfigurationError("EP consumer credential transfer identity is invalid.")
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def _migrate_schema_53(connection: sqlite3.Connection) -> None:
|
|
1073
|
+
"""Transfer consumer credentials to the neutral Server/CENTRAL namespace.
|
|
1074
|
+
|
|
1075
|
+
The entire migration is called from the Server's enclosing immediate
|
|
1076
|
+
transaction. Legacy tables are retained untouched as migration evidence;
|
|
1077
|
+
runtime code switches exclusively to the new tables only after the exact
|
|
1078
|
+
transfer proof succeeds and schema metadata advances.
|
|
1079
|
+
"""
|
|
1080
|
+
|
|
1081
|
+
if _schema_version(connection) != 52:
|
|
1082
|
+
raise ServerConfigurationError("EP consumer credential migration schema version is invalid.")
|
|
1083
|
+
metadata = connection.execute(
|
|
1084
|
+
"SELECT value FROM engineering_metadata WHERE key='installation.schema_version'"
|
|
1085
|
+
).fetchone()
|
|
1086
|
+
if metadata is None or str(metadata[0]) != "52":
|
|
1087
|
+
raise ServerConfigurationError("EP consumer credential migration metadata is invalid.")
|
|
1088
|
+
legacy_credentials, legacy_registrations = (
|
|
1089
|
+
"local_api_credentials", "local_api_consumer_registrations",
|
|
1090
|
+
)
|
|
1091
|
+
current_credentials, current_registrations = (
|
|
1092
|
+
"ep_consumer_credentials", "ep_consumer_registrations",
|
|
1093
|
+
)
|
|
1094
|
+
tables = _table_names(connection)
|
|
1095
|
+
legacy = {legacy_credentials, legacy_registrations} & tables
|
|
1096
|
+
current = {current_credentials, current_registrations} & tables
|
|
1097
|
+
if legacy and legacy != {legacy_credentials, legacy_registrations}:
|
|
1098
|
+
raise ServerConfigurationError("EP consumer credential migration source is incomplete.")
|
|
1099
|
+
if current and current != {current_credentials, current_registrations}:
|
|
1100
|
+
raise ServerConfigurationError("EP consumer credential migration destination is incomplete.")
|
|
1101
|
+
if legacy and current:
|
|
1102
|
+
raise ServerConfigurationError("EP consumer credential migration has ambiguous parallel authority.")
|
|
1103
|
+
if legacy:
|
|
1104
|
+
_require_consumer_table_shape(
|
|
1105
|
+
connection, credentials=legacy_credentials, registrations=legacy_registrations,
|
|
1106
|
+
)
|
|
1107
|
+
_install_ep_consumer_schema(connection)
|
|
1108
|
+
connection.execute(
|
|
1109
|
+
"INSERT INTO ep_consumer_registrations(consumer_id,project_id,status,created_at,updated_at,disabled_at,revoked_at,audit_metadata) "
|
|
1110
|
+
"SELECT consumer_id,project_id,status,created_at,updated_at,disabled_at,revoked_at,audit_metadata "
|
|
1111
|
+
"FROM local_api_consumer_registrations"
|
|
1112
|
+
)
|
|
1113
|
+
connection.execute(
|
|
1114
|
+
"INSERT INTO ep_consumer_credentials(credential_id,consumer_id,project_id,verifier,fingerprint,issued_at,expires_at,revoked_at,replaced_by_credential_id) "
|
|
1115
|
+
"SELECT credential_id,consumer_id,project_id,verifier,fingerprint,issued_at,expires_at,revoked_at,replaced_by_credential_id "
|
|
1116
|
+
"FROM local_api_credentials"
|
|
1117
|
+
)
|
|
1118
|
+
_assert_exact_consumer_transfer(
|
|
1119
|
+
connection, source=legacy_registrations, destination=current_registrations,
|
|
1120
|
+
columns=_CONSUMER_REGISTRATION_COLUMNS,
|
|
1121
|
+
)
|
|
1122
|
+
_assert_exact_consumer_transfer(
|
|
1123
|
+
connection, source=legacy_credentials, destination=current_credentials,
|
|
1124
|
+
columns=_CONSUMER_CREDENTIAL_COLUMNS,
|
|
1125
|
+
)
|
|
1126
|
+
elif current:
|
|
1127
|
+
_require_consumer_table_shape(
|
|
1128
|
+
connection, credentials=current_credentials, registrations=current_registrations,
|
|
1129
|
+
)
|
|
1130
|
+
else:
|
|
1131
|
+
raise ServerConfigurationError("EP consumer credential migration source is absent.")
|
|
1132
|
+
# This table is referenced by the schema-52 receipt provenance table. Keep
|
|
1133
|
+
# those foreign-key declarations pointed at the canonical name while the
|
|
1134
|
+
# installation CHECK constraint is widened for schema 53. Without this
|
|
1135
|
+
# SQLite rewrites a dependent reference to the temporary table name, which
|
|
1136
|
+
# would leave the completed store structurally invalid after that temporary
|
|
1137
|
+
# table is retired.
|
|
1138
|
+
connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema52")
|
|
1139
|
+
connection.execute(
|
|
1140
|
+
"CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, "
|
|
1141
|
+
"schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51,52,53)))"
|
|
1142
|
+
)
|
|
1143
|
+
connection.execute(
|
|
1144
|
+
"INSERT INTO ep_installations(instance_id,created_at,schema_version) "
|
|
1145
|
+
"SELECT instance_id,created_at,53 FROM ep_installations_schema52"
|
|
1146
|
+
)
|
|
1147
|
+
connection.execute("DROP TABLE ep_installations_schema52")
|
|
1148
|
+
connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(53)")
|
|
1149
|
+
connection.execute("UPDATE engineering_metadata SET value='53' WHERE key='installation.schema_version'")
|
|
1150
|
+
connection.execute("UPDATE ep_installations SET schema_version=53")
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def validate_store(data_root: Path, identity: RuntimeIdentity) -> dict[str, object]:
|
|
1154
|
+
"""Return a deterministic fail-closed current-schema structural report."""
|
|
1155
|
+
path = data_root / SERVER_DATABASE_FILENAME
|
|
1156
|
+
try:
|
|
1157
|
+
with sqlite3.connect(f"file:{path}?mode=ro", uri=True) as connection:
|
|
1158
|
+
tables = _table_names(connection)
|
|
1159
|
+
indexes = _index_names(connection)
|
|
1160
|
+
schema = _schema_version(connection)
|
|
1161
|
+
integrity = [str(row[0]) for row in connection.execute("PRAGMA integrity_check")]
|
|
1162
|
+
metadata = dict(connection.execute("SELECT key,value FROM engineering_metadata WHERE key IN ('installation.instance_id','installation.schema_version')"))
|
|
1163
|
+
installation = connection.execute("SELECT instance_id FROM ep_installations WHERE instance_id=?", (identity.instance_id,)).fetchone()
|
|
1164
|
+
except (OSError, sqlite3.DatabaseError) as error:
|
|
1165
|
+
raise ServerConfigurationError("EP Server store is unavailable.") from error
|
|
1166
|
+
valid = schema == SERVER_STORE_SCHEMA_VERSION and SERVER_REQUIRED_TABLES <= tables and SERVER_REQUIRED_INDEXES <= indexes and integrity == ["ok"] and metadata == {"installation.instance_id": identity.instance_id, "installation.schema_version": str(SERVER_STORE_SCHEMA_VERSION)} and installation is not None
|
|
1167
|
+
if not valid:
|
|
1168
|
+
raise ServerConfigurationError(
|
|
1169
|
+
f"EP Server store is not a valid official schema-{SERVER_STORE_SCHEMA_VERSION} installation."
|
|
1170
|
+
)
|
|
1171
|
+
return {"schema_version": schema, "integrity": "PASS", "required_tables": sorted(SERVER_REQUIRED_TABLES), "required_indexes": sorted(SERVER_REQUIRED_INDEXES)}
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def initialize(data_root: Path, *, bind_host: str = "127.0.0.1", bind_port: int = 8765) -> RuntimeIdentity:
|
|
1175
|
+
"""Create or validate an empty, installation-owned server instance."""
|
|
1176
|
+
data_root = data_root.resolve()
|
|
1177
|
+
data_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
1178
|
+
config_path = data_root / SERVER_CONFIGURATION_FILENAME
|
|
1179
|
+
if not config_path.exists():
|
|
1180
|
+
if bind_host != "127.0.0.1" or not 1 <= bind_port <= 65535:
|
|
1181
|
+
raise ServerConfigurationError("EP Server initial bind configuration is invalid.")
|
|
1182
|
+
_write_json(config_path, asdict(ServerConfiguration(
|
|
1183
|
+
SERVER_CONFIGURATION_VERSION, bind_host, bind_port,
|
|
1184
|
+
str(default_engineering_platform_codex_cli_prefix()),
|
|
1185
|
+
)))
|
|
1186
|
+
configuration = ServerConfiguration.load(data_root)
|
|
1187
|
+
# Version 1 inferred the CLI installation at each process boundary from
|
|
1188
|
+
# HOME. Upgrade it once, under the server's stable account identity, so
|
|
1189
|
+
# child workers and later restarts inherit one installation authority.
|
|
1190
|
+
if configuration.version != SERVER_CONFIGURATION_VERSION:
|
|
1191
|
+
configuration = ServerConfiguration(
|
|
1192
|
+
SERVER_CONFIGURATION_VERSION,
|
|
1193
|
+
configuration.bind_host,
|
|
1194
|
+
configuration.bind_port,
|
|
1195
|
+
configuration.managed_codex_cli_prefix,
|
|
1196
|
+
)
|
|
1197
|
+
_write_json(config_path, asdict(configuration))
|
|
1198
|
+
identity_path = data_root / SERVER_IDENTITY_FILENAME
|
|
1199
|
+
if identity_path.exists():
|
|
1200
|
+
try:
|
|
1201
|
+
raw = json.loads(identity_path.read_text(encoding="utf-8"))
|
|
1202
|
+
identity = RuntimeIdentity(str(raw["instance_id"]), str(raw["created_at"]))
|
|
1203
|
+
if not identity.instance_id or not identity.created_at:
|
|
1204
|
+
raise ValueError("empty identity")
|
|
1205
|
+
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error:
|
|
1206
|
+
raise ServerConfigurationError("EP Server runtime identity is invalid.") from error
|
|
1207
|
+
else:
|
|
1208
|
+
identity = RuntimeIdentity(str(uuid4()), _utcnow())
|
|
1209
|
+
_write_json(identity_path, asdict(identity))
|
|
1210
|
+
database_path = data_root / SERVER_DATABASE_FILENAME
|
|
1211
|
+
if database_path.exists():
|
|
1212
|
+
try:
|
|
1213
|
+
with sqlite3.connect(f"file:{database_path}?mode=ro", uri=True) as existing:
|
|
1214
|
+
existing_tables = _table_names(existing)
|
|
1215
|
+
if existing_tables:
|
|
1216
|
+
current_schema = _schema_version(existing)
|
|
1217
|
+
if current_schema not in {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, SERVER_STORE_SCHEMA_VERSION}:
|
|
1218
|
+
raise ServerConfigurationError(
|
|
1219
|
+
f"EP Server store is not a valid official schema-{SERVER_STORE_SCHEMA_VERSION} installation."
|
|
1220
|
+
)
|
|
1221
|
+
if current_schema == SERVER_STORE_SCHEMA_VERSION:
|
|
1222
|
+
validate_store(data_root, identity)
|
|
1223
|
+
return identity
|
|
1224
|
+
if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52}:
|
|
1225
|
+
with sqlite3.connect(database_path) as connection:
|
|
1226
|
+
# Schema-49 rebuilds the submission parent table
|
|
1227
|
+
# to widen its immutable transport constraint.
|
|
1228
|
+
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1229
|
+
connection.execute("PRAGMA legacy_alter_table=ON")
|
|
1230
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1231
|
+
if current_schema == 42:
|
|
1232
|
+
_migrate_schema_43(connection)
|
|
1233
|
+
if current_schema in {42, 43}:
|
|
1234
|
+
_migrate_schema_44(connection)
|
|
1235
|
+
if current_schema in {42, 43, 44}:
|
|
1236
|
+
_migrate_schema_45(connection)
|
|
1237
|
+
if current_schema in {42, 43, 44, 45}:
|
|
1238
|
+
_migrate_schema_46(connection)
|
|
1239
|
+
if current_schema in {42, 43, 44, 45, 46}:
|
|
1240
|
+
_migrate_schema_47(connection)
|
|
1241
|
+
if current_schema in {42, 43, 44, 45, 46, 47}:
|
|
1242
|
+
_migrate_schema_48(connection)
|
|
1243
|
+
if current_schema in {42, 43, 44, 45, 46, 47, 48}:
|
|
1244
|
+
_migrate_schema_49(connection)
|
|
1245
|
+
if current_schema in {42, 43, 44, 45, 46, 47, 48, 49}:
|
|
1246
|
+
_migrate_schema_50(connection)
|
|
1247
|
+
if current_schema != 51:
|
|
1248
|
+
_migrate_schema_51(connection)
|
|
1249
|
+
if current_schema != 52:
|
|
1250
|
+
_migrate_schema_52(connection)
|
|
1251
|
+
_migrate_schema_53(connection)
|
|
1252
|
+
connection.execute("COMMIT")
|
|
1253
|
+
connection.execute("PRAGMA legacy_alter_table=OFF")
|
|
1254
|
+
validate_store(data_root, identity)
|
|
1255
|
+
return identity
|
|
1256
|
+
except sqlite3.DatabaseError as error:
|
|
1257
|
+
raise ServerConfigurationError("EP Server store is unavailable.") from error
|
|
1258
|
+
with sqlite3.connect(database_path) as connection:
|
|
1259
|
+
# Schema-53 widens ep_installations while schema-52 provenance already
|
|
1260
|
+
# references it. SQLite must retain those declarations at the
|
|
1261
|
+
# canonical name during the enclosing rebuild transaction.
|
|
1262
|
+
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1263
|
+
connection.execute("PRAGMA legacy_alter_table=ON")
|
|
1264
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1265
|
+
_install_schema_41(connection, identity)
|
|
1266
|
+
_migrate_schema_42(connection)
|
|
1267
|
+
_migrate_schema_43(connection)
|
|
1268
|
+
_migrate_schema_44(connection)
|
|
1269
|
+
_migrate_schema_45(connection)
|
|
1270
|
+
_migrate_schema_46(connection)
|
|
1271
|
+
_migrate_schema_47(connection)
|
|
1272
|
+
_migrate_schema_48(connection)
|
|
1273
|
+
_migrate_schema_49(connection)
|
|
1274
|
+
_migrate_schema_50(connection)
|
|
1275
|
+
_migrate_schema_51(connection)
|
|
1276
|
+
_migrate_schema_52(connection)
|
|
1277
|
+
_migrate_schema_53(connection)
|
|
1278
|
+
connection.execute("COMMIT")
|
|
1279
|
+
connection.execute("PRAGMA legacy_alter_table=OFF")
|
|
1280
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
1281
|
+
database_path.chmod(0o600)
|
|
1282
|
+
validate_store(data_root, identity)
|
|
1283
|
+
return identity
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _runtime(data_root: Path) -> dict[str, object] | None:
|
|
1287
|
+
try:
|
|
1288
|
+
raw = json.loads((data_root / SERVER_RUNTIME_FILENAME).read_text(encoding="utf-8"))
|
|
1289
|
+
return raw if isinstance(raw, dict) else None
|
|
1290
|
+
except (OSError, json.JSONDecodeError):
|
|
1291
|
+
return None
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _alive(pid: object) -> bool:
|
|
1295
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
1296
|
+
return False
|
|
1297
|
+
try:
|
|
1298
|
+
os.kill(pid, 0)
|
|
1299
|
+
except ProcessLookupError:
|
|
1300
|
+
return False
|
|
1301
|
+
except PermissionError:
|
|
1302
|
+
return True
|
|
1303
|
+
return True
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def _transport_components(data_root: Path, *, server_running: bool) -> dict[str, dict[str, object]]:
|
|
1307
|
+
"""Return secret-free, platform-scoped ingress observability from CENTRAL.
|
|
1308
|
+
|
|
1309
|
+
Submission timestamps are observation only. They never change queue or
|
|
1310
|
+
execution semantics and intentionally retain no credential or raw prompt.
|
|
1311
|
+
"""
|
|
1312
|
+
latest: dict[str, str] = {}
|
|
1313
|
+
with sqlite3.connect(f"file:{data_root / SERVER_DATABASE_FILENAME}?mode=ro", uri=True) as connection:
|
|
1314
|
+
for transport, created_at in connection.execute(
|
|
1315
|
+
"SELECT transport,MAX(created_at) FROM ep_submissions GROUP BY transport"
|
|
1316
|
+
):
|
|
1317
|
+
latest[str(transport)] = str(created_at)
|
|
1318
|
+
http_status_code = "HTTP_INGRESS_HEALTHY" if server_running else "HTTP_INGRESS_DOWN"
|
|
1319
|
+
cli_status_code = "CLI_INGRESS_AVAILABLE" if server_running else "CLI_INGRESS_DEGRADED"
|
|
1320
|
+
file_last = latest.get("FILE_INBOX")
|
|
1321
|
+
dependabot_last = latest.get("DEPENDABOT")
|
|
1322
|
+
heartbeat = file_inbox.read_heartbeat(data_root / FILE_INBOX_DIRECTORY)
|
|
1323
|
+
heartbeat_at = str(heartbeat.get("updated_at", "")) if heartbeat else ""
|
|
1324
|
+
try:
|
|
1325
|
+
heartbeat_fresh = (datetime.now(timezone.utc) - datetime.fromisoformat(heartbeat_at)).total_seconds() <= 10
|
|
1326
|
+
except ValueError:
|
|
1327
|
+
heartbeat_fresh = False
|
|
1328
|
+
delivery_retry = str(heartbeat.get("delivery_retry", "NONE")) if heartbeat else "NONE"
|
|
1329
|
+
submission_ready = bool(heartbeat and heartbeat.get("state") == "READY" and heartbeat.get("readiness") == "SUBMISSION_CAPABLE")
|
|
1330
|
+
quarantine_count = int(heartbeat.get("quarantine_count", 0)) if heartbeat and isinstance(heartbeat.get("quarantine_count", 0), int) else 0
|
|
1331
|
+
recent_error = heartbeat.get("recent_error") if heartbeat else None
|
|
1332
|
+
# A live watcher with pending delivery, a bounded adapter diagnostic, or
|
|
1333
|
+
# quarantined ingress is operational but needs attention. It is not a
|
|
1334
|
+
# CENTRAL execution/run failure and cannot affect queue authority.
|
|
1335
|
+
file_attention_needed = delivery_retry != "NONE" or bool(recent_error) or quarantine_count > 0
|
|
1336
|
+
file_status_code = (
|
|
1337
|
+
"FILE_INGRESS_STOPPED" if not server_running or not heartbeat_fresh
|
|
1338
|
+
else "FILE_INGRESS_NOT_READY" if not submission_ready
|
|
1339
|
+
else "FILE_INGRESS_DEGRADED" if file_attention_needed else "FILE_INGRESS_RUNNING"
|
|
1340
|
+
)
|
|
1341
|
+
dependabot_heartbeat = dependabot_producer.read_heartbeat(data_root)
|
|
1342
|
+
dependabot_updated = str(dependabot_heartbeat.get("updated_at", "")) if dependabot_heartbeat else ""
|
|
1343
|
+
try:
|
|
1344
|
+
dependabot_fresh = (datetime.now(timezone.utc) - datetime.fromisoformat(dependabot_updated)).total_seconds() <= 310
|
|
1345
|
+
except ValueError:
|
|
1346
|
+
dependabot_fresh = False
|
|
1347
|
+
dependabot_ready = bool(
|
|
1348
|
+
server_running
|
|
1349
|
+
and dependabot_fresh
|
|
1350
|
+
and dependabot_heartbeat
|
|
1351
|
+
and dependabot_heartbeat.get("state") == "READY"
|
|
1352
|
+
and dependabot_heartbeat.get("readiness") == "DISCOVERY_CAPABLE"
|
|
1353
|
+
)
|
|
1354
|
+
return {
|
|
1355
|
+
"http_ingress": {
|
|
1356
|
+
"healthy": server_running, "status_code": http_status_code,
|
|
1357
|
+
"detail_code": "CENTRAL_LISTENER_ENDPOINT" if server_running else "CENTRAL_LISTENER_UNAVAILABLE",
|
|
1358
|
+
"version": "1", # canonical submission protocol version
|
|
1359
|
+
"last_successful_submission": latest.get("HTTP"), "recent_error": None,
|
|
1360
|
+
},
|
|
1361
|
+
"cli_ingress": {
|
|
1362
|
+
"healthy": server_running, "status_code": cli_status_code,
|
|
1363
|
+
"detail_code": "CANONICAL_SUBMISSION_COMPATIBILITY" if server_running else "CENTRAL_ENDPOINT_UNAVAILABLE",
|
|
1364
|
+
"version": "1", "last_successful_submission": latest.get("CLI"), "recent_error": None,
|
|
1365
|
+
},
|
|
1366
|
+
"file_inbox_ingress": {
|
|
1367
|
+
"healthy": file_status_code == "FILE_INGRESS_RUNNING", "status_code": file_status_code,
|
|
1368
|
+
"detail_code": "FILE_INBOX_HEARTBEAT" if server_running and heartbeat_fresh else "FILE_INBOX_HEARTBEAT_MISSING",
|
|
1369
|
+
"watched_location": heartbeat.get("watched_location") if heartbeat else str(data_root / FILE_INBOX_DIRECTORY),
|
|
1370
|
+
"heartbeat": heartbeat_at or None,
|
|
1371
|
+
"last_successful_submission": file_last,
|
|
1372
|
+
"delivery_retry_code": f"FILE_INGRESS_DELIVERY_RETRY_{delivery_retry}",
|
|
1373
|
+
"quarantine_count": quarantine_count,
|
|
1374
|
+
# Never transport a raw exception into a presentation projection.
|
|
1375
|
+
"reason_code": "FILE_INBOX_DIAGNOSTIC" if recent_error else None,
|
|
1376
|
+
},
|
|
1377
|
+
"dependabot_producer": {
|
|
1378
|
+
"healthy": dependabot_ready,
|
|
1379
|
+
"status_code": "DEPENDABOT_READY" if dependabot_ready else "DEPENDABOT_DEGRADED",
|
|
1380
|
+
"detail_code": "DEPENDABOT_HEARTBEAT" if dependabot_fresh else "DEPENDABOT_HEARTBEAT_MISSING",
|
|
1381
|
+
"heartbeat": dependabot_updated or None,
|
|
1382
|
+
"last_successful_submission": dependabot_last,
|
|
1383
|
+
"reason_code": "DEPENDABOT_DIAGNOSTIC" if dependabot_heartbeat and dependabot_heartbeat.get("recent_error") else None,
|
|
1384
|
+
},
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _dashboard_relay_component(*, server_running: bool) -> dict[str, object]:
|
|
1389
|
+
"""Project the Relay only when its real lifecycle owner is live.
|
|
1390
|
+
|
|
1391
|
+
The relay is an optional access adapter, but it is still a logical
|
|
1392
|
+
Platform Component. A running EP Server cannot stand in for a missing or
|
|
1393
|
+
repeatedly exiting LaunchAgent.
|
|
1394
|
+
"""
|
|
1395
|
+
definition = PLATFORM_COMPONENT_BY_ID["dashboard_relay"]
|
|
1396
|
+
label = definition.lifecycle_label
|
|
1397
|
+
try:
|
|
1398
|
+
lifecycle = LaunchdProvider()
|
|
1399
|
+
runtime = lifecycle.runtime_status(label) if label else None
|
|
1400
|
+
observed = lifecycle.runtime_details(label) if label else None
|
|
1401
|
+
observed = observed if isinstance(observed, LaunchdRuntimeDetails) else None
|
|
1402
|
+
relay_running = bool(runtime and runtime.qualified)
|
|
1403
|
+
detail = runtime.detail if runtime is not None else "lifecycle owner unavailable"
|
|
1404
|
+
except OSError:
|
|
1405
|
+
observed, relay_running, detail = None, False, "lifecycle owner unavailable"
|
|
1406
|
+
healthy = server_running and relay_running
|
|
1407
|
+
if healthy:
|
|
1408
|
+
detail_code = "DASHBOARD_RELAY_TAILSCALE_AVAILABLE"
|
|
1409
|
+
elif observed is not None and not observed.loaded:
|
|
1410
|
+
detail_code = "DASHBOARD_RELAY_LAUNCH_AGENT_UNLOADED"
|
|
1411
|
+
elif observed is not None and not observed.active:
|
|
1412
|
+
detail_code = "DASHBOARD_RELAY_PROCESS_INACTIVE"
|
|
1413
|
+
else:
|
|
1414
|
+
detail_code = "DASHBOARD_RELAY_LIFECYCLE_UNAVAILABLE"
|
|
1415
|
+
return {
|
|
1416
|
+
"healthy": healthy,
|
|
1417
|
+
"status_code": definition.active_status if healthy else definition.inactive_status,
|
|
1418
|
+
"detail_code": detail_code,
|
|
1419
|
+
"lifecycle_label": label,
|
|
1420
|
+
"lifecycle_state": "RUNNING" if relay_running else "STOPPED",
|
|
1421
|
+
# A Relay has its own LaunchAgent process. Never substitute a
|
|
1422
|
+
# placeholder (notably zero) for the observed process lifetime.
|
|
1423
|
+
"uptime_seconds": observed.uptime_seconds if observed is not None else None,
|
|
1424
|
+
"recent_error": None if healthy else detail,
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _launch_agent_configuration(plist_path: Path) -> dict[str, object]:
|
|
1429
|
+
"""Project the safe start policy from one EP-owned LaunchAgent plist."""
|
|
1430
|
+
try:
|
|
1431
|
+
payload = plistlib.loads(plist_path.read_bytes())
|
|
1432
|
+
except (OSError, plistlib.InvalidFileException):
|
|
1433
|
+
return {}
|
|
1434
|
+
if not isinstance(payload, dict):
|
|
1435
|
+
return {}
|
|
1436
|
+
configuration: dict[str, object] = {}
|
|
1437
|
+
run_at_load = payload.get("RunAtLoad")
|
|
1438
|
+
if isinstance(run_at_load, bool):
|
|
1439
|
+
configuration["run_at_load"] = run_at_load
|
|
1440
|
+
keep_alive = payload.get("KeepAlive")
|
|
1441
|
+
if isinstance(keep_alive, (bool, dict)):
|
|
1442
|
+
configuration["keep_alive"] = bool(keep_alive)
|
|
1443
|
+
return configuration
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _platform_component_detail(data_root: Path, component_id: str) -> dict[str, object] | None:
|
|
1447
|
+
"""Expose one secret-free detail view from the same platform projection."""
|
|
1448
|
+
component = status(data_root)["components"].get(component_id) # type: ignore[index]
|
|
1449
|
+
if not isinstance(component, dict):
|
|
1450
|
+
return None
|
|
1451
|
+
definition = PLATFORM_COMPONENT_BY_ID[component_id]
|
|
1452
|
+
service_paths = server_service.default_paths(data_root)
|
|
1453
|
+
runtime_executable = Path(sys.executable).expanduser().absolute()
|
|
1454
|
+
installation: dict[str, str] = {}
|
|
1455
|
+
if component_id == "ep_server":
|
|
1456
|
+
installation = {
|
|
1457
|
+
"runtime_path": str(runtime_executable.parent.parent),
|
|
1458
|
+
"central_data_path": str(data_root.resolve()),
|
|
1459
|
+
"launch_agent_path": str(service_paths.plist_path),
|
|
1460
|
+
"error_log_path": str(service_paths.stderr_log),
|
|
1461
|
+
}
|
|
1462
|
+
elif component_id == "platform_database":
|
|
1463
|
+
database_path = data_root / SERVER_DATABASE_FILENAME
|
|
1464
|
+
installation = {
|
|
1465
|
+
"central_data_path": str(data_root.resolve()),
|
|
1466
|
+
"database_path": str(database_path.resolve()),
|
|
1467
|
+
}
|
|
1468
|
+
elif component_id == "dashboard_relay":
|
|
1469
|
+
installation = {
|
|
1470
|
+
"launch_agent_path": str(server_relay.launch_agent_path()),
|
|
1471
|
+
"relay_binary_path": str(server_relay.relay_binary(data_root)),
|
|
1472
|
+
}
|
|
1473
|
+
lifecycle_label = server_service.LABEL if component_id == "ep_server" else definition.lifecycle_label
|
|
1474
|
+
observed: LaunchdRuntimeDetails | None = None
|
|
1475
|
+
if lifecycle_label:
|
|
1476
|
+
candidate = LaunchdProvider().runtime_details(lifecycle_label)
|
|
1477
|
+
observed = candidate if isinstance(candidate, LaunchdRuntimeDetails) else None
|
|
1478
|
+
detail: dict[str, object] = {
|
|
1479
|
+
"component": component_id,
|
|
1480
|
+
"machine": os.uname().nodename,
|
|
1481
|
+
"restart_supported": definition.restart_supported,
|
|
1482
|
+
"installation": installation,
|
|
1483
|
+
**component,
|
|
1484
|
+
}
|
|
1485
|
+
if component_id == "http_ingress":
|
|
1486
|
+
detail["swagger_endpoint"] = HTTP_JSON_OPENAPI_PATH
|
|
1487
|
+
if observed is not None:
|
|
1488
|
+
detail["launchd"] = {
|
|
1489
|
+
"label": observed.label,
|
|
1490
|
+
"loaded": observed.loaded,
|
|
1491
|
+
"active": observed.active,
|
|
1492
|
+
"pid": observed.pid,
|
|
1493
|
+
"last_exit_code": observed.last_exit_code,
|
|
1494
|
+
**(
|
|
1495
|
+
_launch_agent_configuration(Path(installation["launch_agent_path"]))
|
|
1496
|
+
if isinstance(installation.get("launch_agent_path"), str) else {}
|
|
1497
|
+
),
|
|
1498
|
+
}
|
|
1499
|
+
detail["process_state"] = "OWNED_PROCESS"
|
|
1500
|
+
detail["processes"] = ([{
|
|
1501
|
+
"pid": observed.pid,
|
|
1502
|
+
"memory_kib": observed.memory_kib,
|
|
1503
|
+
}] if observed.active and observed.pid is not None else [])
|
|
1504
|
+
if observed.uptime_seconds is not None:
|
|
1505
|
+
detail["uptime_seconds"] = observed.uptime_seconds
|
|
1506
|
+
elif component_id == "platform_database":
|
|
1507
|
+
detail["launchd"] = {}
|
|
1508
|
+
detail["process_state"] = "STORAGE"
|
|
1509
|
+
try:
|
|
1510
|
+
detail["database_size_bytes"] = (data_root / SERVER_DATABASE_FILENAME).stat().st_size
|
|
1511
|
+
except OSError:
|
|
1512
|
+
pass
|
|
1513
|
+
else:
|
|
1514
|
+
host = LaunchdProvider().runtime_details(server_service.LABEL)
|
|
1515
|
+
host = host if isinstance(host, LaunchdRuntimeDetails) else None
|
|
1516
|
+
detail["launchd"] = {}
|
|
1517
|
+
detail["process_state"] = "IN_PROCESS"
|
|
1518
|
+
detail["process_host"] = {
|
|
1519
|
+
"component": "ep_server",
|
|
1520
|
+
"pid": host.pid if host is not None and host.active else None,
|
|
1521
|
+
"uptime_seconds": host.uptime_seconds if host is not None and host.active else None,
|
|
1522
|
+
}
|
|
1523
|
+
return detail
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def _restart_platform_component(data_root: Path, component_id: str) -> dict[str, object]:
|
|
1527
|
+
"""Restart one explicitly restartable Platform component through its owner.
|
|
1528
|
+
|
|
1529
|
+
Component identifiers and lifecycle labels come exclusively from the
|
|
1530
|
+
canonical model. This deliberately does not expose a generic process or
|
|
1531
|
+
LaunchAgent control endpoint.
|
|
1532
|
+
"""
|
|
1533
|
+
definition = PLATFORM_COMPONENT_BY_ID.get(component_id)
|
|
1534
|
+
if definition is None or not definition.restart_supported or not definition.lifecycle_label:
|
|
1535
|
+
raise ValueError("COMPONENT_RESTART_NOT_SUPPORTED")
|
|
1536
|
+
logger = component_logger(
|
|
1537
|
+
data_root,
|
|
1538
|
+
definition.id,
|
|
1539
|
+
central_database=data_root / SERVER_DATABASE_FILENAME,
|
|
1540
|
+
)
|
|
1541
|
+
log_event(
|
|
1542
|
+
logger,
|
|
1543
|
+
logging.INFO,
|
|
1544
|
+
"component_restart_requested",
|
|
1545
|
+
context={"target_component": definition.id},
|
|
1546
|
+
)
|
|
1547
|
+
lifecycle = LaunchdProvider()
|
|
1548
|
+
try:
|
|
1549
|
+
lifecycle.restart(definition.lifecycle_label)
|
|
1550
|
+
# ``kickstart`` only acknowledges the request. Give launchd a small,
|
|
1551
|
+
# bounded interval to spawn the owned process before deciding whether
|
|
1552
|
+
# the repair actually reached its observable postcondition.
|
|
1553
|
+
deadline = time.monotonic() + 2
|
|
1554
|
+
postcondition = lifecycle.runtime_status(definition.lifecycle_label)
|
|
1555
|
+
while not postcondition.qualified and time.monotonic() < deadline:
|
|
1556
|
+
time.sleep(.1)
|
|
1557
|
+
postcondition = lifecycle.runtime_status(definition.lifecycle_label)
|
|
1558
|
+
if not postcondition.qualified:
|
|
1559
|
+
raise OSError("COMPONENT_RESTART_POSTCONDITION_FAILED")
|
|
1560
|
+
except OSError as error:
|
|
1561
|
+
log_event(
|
|
1562
|
+
logger,
|
|
1563
|
+
logging.WARNING,
|
|
1564
|
+
"component_restart_failed",
|
|
1565
|
+
diagnostic=str(error),
|
|
1566
|
+
context={"target_component": definition.id},
|
|
1567
|
+
)
|
|
1568
|
+
raise
|
|
1569
|
+
log_event(
|
|
1570
|
+
logger,
|
|
1571
|
+
logging.INFO,
|
|
1572
|
+
"component_restart_completed",
|
|
1573
|
+
context={
|
|
1574
|
+
"target_component": definition.id,
|
|
1575
|
+
"postcondition": "LIFECYCLE_OWNER_RUNNING",
|
|
1576
|
+
},
|
|
1577
|
+
)
|
|
1578
|
+
return {
|
|
1579
|
+
"restarting": definition.id,
|
|
1580
|
+
"scope": "PLATFORM",
|
|
1581
|
+
"postcondition": "LIFECYCLE_OWNER_RUNNING",
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _audit_configuration_change(
|
|
1586
|
+
data_root: Path,
|
|
1587
|
+
*,
|
|
1588
|
+
scope: str,
|
|
1589
|
+
key: str,
|
|
1590
|
+
previous: object,
|
|
1591
|
+
value: object,
|
|
1592
|
+
) -> None:
|
|
1593
|
+
"""Persist a bounded CENTRAL audit event for a successful setting change."""
|
|
1594
|
+
log_event(
|
|
1595
|
+
component_logger(
|
|
1596
|
+
data_root,
|
|
1597
|
+
"operations_console",
|
|
1598
|
+
central_database=data_root / SERVER_DATABASE_FILENAME,
|
|
1599
|
+
),
|
|
1600
|
+
logging.INFO,
|
|
1601
|
+
"configuration_changed",
|
|
1602
|
+
context={
|
|
1603
|
+
"configuration_scope": scope,
|
|
1604
|
+
"configuration_key": key,
|
|
1605
|
+
"previous_value": previous,
|
|
1606
|
+
"new_value": value,
|
|
1607
|
+
},
|
|
1608
|
+
)
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def status(data_root: Path) -> dict[str, object]:
|
|
1612
|
+
identity = initialize(data_root)
|
|
1613
|
+
config = ServerConfiguration.load(data_root)
|
|
1614
|
+
runtime = _runtime(data_root)
|
|
1615
|
+
running = bool(runtime and _alive(runtime.get("pid")))
|
|
1616
|
+
components = _transport_components(data_root, server_running=running)
|
|
1617
|
+
components["dashboard_relay"] = _dashboard_relay_component(server_running=running)
|
|
1618
|
+
# One Server-native inventory feeds Components, the titlebar popout and
|
|
1619
|
+
# detail modals. It deliberately contains no watcher/check-out model.
|
|
1620
|
+
for definition in PLATFORM_COMPONENTS:
|
|
1621
|
+
if definition.id in components:
|
|
1622
|
+
components[definition.id].update({
|
|
1623
|
+
"kind": definition.kind, "name_key": definition.name_key,
|
|
1624
|
+
"group": definition.group, "critical": definition.critical, "restart_supported": definition.restart_supported,
|
|
1625
|
+
"log_component": definition.id,
|
|
1626
|
+
})
|
|
1627
|
+
continue
|
|
1628
|
+
healthy = True if definition.id == "platform_database" else running
|
|
1629
|
+
components[definition.id] = {
|
|
1630
|
+
"kind": definition.kind, "name_key": definition.name_key,
|
|
1631
|
+
"group": definition.group, "critical": definition.critical, "restart_supported": definition.restart_supported,
|
|
1632
|
+
"log_component": definition.id, "healthy": healthy,
|
|
1633
|
+
"status_code": definition.active_status if healthy else definition.inactive_status,
|
|
1634
|
+
"detail_code": definition.detail_code,
|
|
1635
|
+
**({"version": str(SERVER_STORE_SCHEMA_VERSION)} if definition.id == "platform_database" else {}),
|
|
1636
|
+
}
|
|
1637
|
+
server_component = components["ep_server"]
|
|
1638
|
+
server_component["version"] = _console_platform_version()
|
|
1639
|
+
started_at = runtime.get("started_at") if runtime else None
|
|
1640
|
+
if isinstance(started_at, str):
|
|
1641
|
+
try:
|
|
1642
|
+
server_component["uptime_seconds"] = max(
|
|
1643
|
+
0, int((datetime.now(timezone.utc) - datetime.fromisoformat(started_at)).total_seconds())
|
|
1644
|
+
)
|
|
1645
|
+
except ValueError:
|
|
1646
|
+
pass
|
|
1647
|
+
# Every canonical component is observed and included in the response.
|
|
1648
|
+
# The aggregate follows the component model's explicit criticality: an
|
|
1649
|
+
# optional access or producer adapter remains diagnosable without making
|
|
1650
|
+
# a core Server probe unavailable on a host that does not install it.
|
|
1651
|
+
unhealthy_components = [
|
|
1652
|
+
item.id for item in PLATFORM_COMPONENTS
|
|
1653
|
+
if not bool(components[item.id].get("healthy"))
|
|
1654
|
+
]
|
|
1655
|
+
healthy = all(
|
|
1656
|
+
bool(components[item.id].get("healthy"))
|
|
1657
|
+
for item in PLATFORM_COMPONENTS
|
|
1658
|
+
if item.critical
|
|
1659
|
+
)
|
|
1660
|
+
return {
|
|
1661
|
+
"service": "engineering-platform-server",
|
|
1662
|
+
"healthy": healthy,
|
|
1663
|
+
"health": "ok" if healthy else "degraded",
|
|
1664
|
+
"unhealthy_components": unhealthy_components,
|
|
1665
|
+
"instance_id": identity.instance_id,
|
|
1666
|
+
"store": "ready",
|
|
1667
|
+
"schema_version": SERVER_STORE_SCHEMA_VERSION,
|
|
1668
|
+
"operational_state": "empty-valid",
|
|
1669
|
+
"running": running,
|
|
1670
|
+
"managed_codex_runtime": managed_codex_runtime.inspect(data_root),
|
|
1671
|
+
"lifecycle_worker": {
|
|
1672
|
+
# The worker is hosted by the sole installed Server process. A
|
|
1673
|
+
# stopped process is never reported as an active worker.
|
|
1674
|
+
"state": "RUNNING" if running else "STOPPED",
|
|
1675
|
+
},
|
|
1676
|
+
"bind": {"host": config.bind_host, "port": config.bind_port},
|
|
1677
|
+
"components": components,
|
|
1678
|
+
"component_model": [
|
|
1679
|
+
{"id": item.id, "name_key": item.name_key, "kind": item.kind, "group": item.group,
|
|
1680
|
+
"critical": item.critical, "restart_supported": item.restart_supported,
|
|
1681
|
+
"lifecycle_label": item.lifecycle_label, "log_component": item.id}
|
|
1682
|
+
for item in PLATFORM_COMPONENTS
|
|
1683
|
+
],
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def operations_projection(data_root: Path) -> dict[str, object]:
|
|
1688
|
+
"""Return the installed CENTRAL's secret-free Console projection.
|
|
1689
|
+
|
|
1690
|
+
The selected project remains a browser presentation preference. This
|
|
1691
|
+
endpoint intentionally returns topology only; no checkout path, Agent
|
|
1692
|
+
credential, or execution capability is exposed here.
|
|
1693
|
+
"""
|
|
1694
|
+
identity = initialize(data_root)
|
|
1695
|
+
with sqlite3.connect(f"file:{data_root / SERVER_DATABASE_FILENAME}?mode=ro", uri=True) as connection:
|
|
1696
|
+
topology = project_topology.topology(connection)
|
|
1697
|
+
return {
|
|
1698
|
+
"installation_id": identity.instance_id,
|
|
1699
|
+
"schema_version": SERVER_STORE_SCHEMA_VERSION,
|
|
1700
|
+
"managed_codex_runtime": managed_codex_runtime.inspect(data_root),
|
|
1701
|
+
"projects": topology["projects"],
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def _console_projects(data_root: Path) -> list[dict[str, str]]:
|
|
1706
|
+
"""List CENTRAL project identities without opening their checkouts.
|
|
1707
|
+
|
|
1708
|
+
The selector is a logical CENTRAL projection. A local binding is checked
|
|
1709
|
+
only later, when a user explicitly selects that project for a transitional
|
|
1710
|
+
root-bound route.
|
|
1711
|
+
"""
|
|
1712
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1713
|
+
rows = connection.execute("""SELECT p.project_id, r.repository_id
|
|
1714
|
+
FROM ep_project_registrations AS p
|
|
1715
|
+
JOIN ep_repository_registrations AS r
|
|
1716
|
+
ON r.project_id=p.project_id AND r.role='authority'
|
|
1717
|
+
WHERE p.status='ACTIVE'
|
|
1718
|
+
ORDER BY p.project_id""").fetchall()
|
|
1719
|
+
return [{"project_id": str(project_id), "repository_id": str(repository_id)}
|
|
1720
|
+
for project_id, repository_id in rows]
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def _console_queue_projection(data_root: Path, project_id: str) -> dict[str, object]:
|
|
1724
|
+
"""Read the selected project's single transport-neutral CENTRAL FIFO."""
|
|
1725
|
+
for project in _console_projects(data_root):
|
|
1726
|
+
if project["project_id"] != project_id:
|
|
1727
|
+
continue
|
|
1728
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1729
|
+
context = project_context(
|
|
1730
|
+
connection,
|
|
1731
|
+
data_root=data_root,
|
|
1732
|
+
project_id=project_id,
|
|
1733
|
+
repository_id=project["repository_id"],
|
|
1734
|
+
require_local_root=False,
|
|
1735
|
+
)
|
|
1736
|
+
queue = ParityProjectStore(connection, context).console_queue_projection()
|
|
1737
|
+
rows = connection.execute(
|
|
1738
|
+
"""SELECT run_id,operator_resolution FROM ep_parity_lifecycle_dispatches
|
|
1739
|
+
WHERE project_id=? AND operator_resolution IN ('DISMISSED','RETRIED')""",
|
|
1740
|
+
(project_id,),
|
|
1741
|
+
).fetchall()
|
|
1742
|
+
return {**queue, "operator_handling": {str(run_id): str(resolution) for run_id, resolution in rows}}
|
|
1743
|
+
raise local_repository_binding.LocalRepositoryBindingError("CONSOLE_PROJECT_UNAVAILABLE")
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def _console_platform_version() -> str:
|
|
1747
|
+
"""Read the installed platform version once for CENTRAL Console snapshots."""
|
|
1748
|
+
return EngineeringPlatformManifest.load(
|
|
1749
|
+
package_path("ENGINEERING_PLATFORM_VERSION.json")
|
|
1750
|
+
).platform_version
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def _central_console_project_snapshot(data_root: Path, project_id: str) -> dict[str, object]:
|
|
1754
|
+
"""Return the Slice-B project status/history projection from CENTRAL only."""
|
|
1755
|
+
queue = _console_queue_projection(data_root, project_id)
|
|
1756
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1757
|
+
runs = connection.execute(
|
|
1758
|
+
"""SELECT run_id,state,created_at,updated_at,execution_mode
|
|
1759
|
+
FROM ep_execution_runs WHERE project_id=?
|
|
1760
|
+
ORDER BY created_at DESC,run_id DESC LIMIT 1000""",
|
|
1761
|
+
(project_id,),
|
|
1762
|
+
).fetchall()
|
|
1763
|
+
dispatches = dict(connection.execute(
|
|
1764
|
+
"SELECT run_id,state FROM ep_parity_lifecycle_dispatches WHERE project_id=?",
|
|
1765
|
+
(project_id,),
|
|
1766
|
+
).fetchall())
|
|
1767
|
+
records = [
|
|
1768
|
+
{
|
|
1769
|
+
"run_id": str(run_id), "status": str(dispatches.get(run_id, state)),
|
|
1770
|
+
"state": str(dispatches.get(run_id, state)), "created_at": str(created_at),
|
|
1771
|
+
"updated_at": str(updated_at), "execution_mode": execution_mode,
|
|
1772
|
+
"project_id": project_id,
|
|
1773
|
+
}
|
|
1774
|
+
for run_id, state, created_at, updated_at, execution_mode in runs
|
|
1775
|
+
]
|
|
1776
|
+
active = next((record for record in records if record["state"] in {"CLAIMED", "RUNNING"}), None)
|
|
1777
|
+
return {
|
|
1778
|
+
"project_id": project_id,
|
|
1779
|
+
"scope": "PROJECT",
|
|
1780
|
+
"status": {
|
|
1781
|
+
"project_id": project_id,
|
|
1782
|
+
"platform_version": _console_platform_version(),
|
|
1783
|
+
"queue_depth": queue["queue_depth"],
|
|
1784
|
+
"queue_items": queue["queue_items"],
|
|
1785
|
+
"active_run": active["run_id"] if active else None,
|
|
1786
|
+
"last_executed_run": records[0]["run_id"] if records else None,
|
|
1787
|
+
"lifecycle_source": "CENTRAL",
|
|
1788
|
+
},
|
|
1789
|
+
"runs": records,
|
|
1790
|
+
"queue": queue,
|
|
1791
|
+
"telemetry": _central_console_telemetry(data_root, project_id),
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def _no_project_console_snapshot(data_root: Path) -> dict[str, object]:
|
|
1796
|
+
"""Give the Console a loadable CENTRAL-only snapshot at ``<geen>``.
|
|
1797
|
+
|
|
1798
|
+
The no-project document deliberately hides project state, but the shared
|
|
1799
|
+
dashboard shell still hydrates through the snapshot endpoint. Returning a
|
|
1800
|
+
minimal platform projection prevents it from remaining behind the loading
|
|
1801
|
+
overlay while preserving the fail-closed boundary for all project routes.
|
|
1802
|
+
"""
|
|
1803
|
+
# Aggregate the canonical CENTRAL submission state only. File Inbox files,
|
|
1804
|
+
# watcher backlogs and transport retry diagnostics never affect this count.
|
|
1805
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1806
|
+
queue_depth = int(connection.execute(
|
|
1807
|
+
"SELECT COUNT(*) FROM ep_submissions WHERE state IN ('QUEUED','ADMITTED')"
|
|
1808
|
+
).fetchone()[0])
|
|
1809
|
+
queue = {"operator_handling": {}, "queue_depth": queue_depth, "queue_items": [], "scope": "ALL_PROJECTS"}
|
|
1810
|
+
return {
|
|
1811
|
+
"scope": "PLATFORM",
|
|
1812
|
+
"queue": queue,
|
|
1813
|
+
"runs": [],
|
|
1814
|
+
"telemetry": [],
|
|
1815
|
+
"status": {
|
|
1816
|
+
"lifecycle_source": "CENTRAL",
|
|
1817
|
+
"platform_version": _console_platform_version(),
|
|
1818
|
+
**queue,
|
|
1819
|
+
},
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
def _central_console_run_detail(data_root: Path, project_id: str, run_id: str) -> dict[str, object] | None:
|
|
1824
|
+
"""Resolve a run by canonical project/run identity, never by checkout."""
|
|
1825
|
+
snapshot = _central_console_project_snapshot(data_root, project_id)
|
|
1826
|
+
return next((record for record in snapshot["runs"] if record["run_id"] == run_id), None)
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def _central_console_telemetry(data_root: Path, project_id: str) -> list[dict[str, object]]:
|
|
1830
|
+
"""Read bounded daily telemetry through CENTRAL's run/project lineage."""
|
|
1831
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1832
|
+
rows = connection.execute(
|
|
1833
|
+
"""SELECT r.execution_date,COUNT(*),
|
|
1834
|
+
SUM(r.terminal_state='COMPLETE'),SUM(r.terminal_state='BLOCKED'),SUM(r.terminal_state='FAILED'),
|
|
1835
|
+
AVG(r.execution_seconds),AVG(r.total_execution_seconds),AVG(r.queue_wait_seconds),
|
|
1836
|
+
SUM(r.input_tokens),SUM(r.output_tokens),SUM(r.total_tokens)
|
|
1837
|
+
FROM execution_runs AS r
|
|
1838
|
+
JOIN ep_parity_lifecycle_dispatches AS d ON d.run_id=r.run_id
|
|
1839
|
+
WHERE d.project_id=?
|
|
1840
|
+
GROUP BY r.execution_date ORDER BY r.execution_date DESC LIMIT 360""",
|
|
1841
|
+
(project_id,),
|
|
1842
|
+
).fetchall()
|
|
1843
|
+
keys = (
|
|
1844
|
+
"date", "prompt_count", "complete_count", "blocked_count", "failed_count",
|
|
1845
|
+
"average_execution_seconds", "average_total_execution_seconds", "average_queue_wait_seconds",
|
|
1846
|
+
"input_tokens", "output_tokens", "total_tokens",
|
|
1847
|
+
)
|
|
1848
|
+
return [dict(zip(keys, row, strict=True)) | {
|
|
1849
|
+
"average_provider_execution_seconds": None, "average_validation_seconds": None,
|
|
1850
|
+
} for row in rows]
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _central_console_telemetry_detail(data_root: Path, project_id: str, execution_date: str) -> dict[str, object] | None:
|
|
1854
|
+
"""Provide a project-isolated CENTRAL telemetry day without root fallback."""
|
|
1855
|
+
if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", execution_date):
|
|
1856
|
+
return None
|
|
1857
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1858
|
+
rows = connection.execute(
|
|
1859
|
+
"""SELECT r.run_id,r.execution_started_at,r.terminal_state,r.total_execution_seconds,
|
|
1860
|
+
r.queue_wait_seconds,r.runtime_provider,r.runtime_model,r.reasoning_profile,
|
|
1861
|
+
r.producer_type,r.repository
|
|
1862
|
+
FROM execution_runs AS r
|
|
1863
|
+
JOIN ep_parity_lifecycle_dispatches AS d ON d.run_id=r.run_id
|
|
1864
|
+
WHERE d.project_id=? AND r.execution_date=?
|
|
1865
|
+
ORDER BY r.execution_started_at DESC LIMIT 250""",
|
|
1866
|
+
(project_id, execution_date),
|
|
1867
|
+
).fetchall()
|
|
1868
|
+
if not rows:
|
|
1869
|
+
return None
|
|
1870
|
+
run_rows = [{
|
|
1871
|
+
"run_id": str(row[0]), "started_at": row[1], "status": row[2],
|
|
1872
|
+
"total_duration_ms": round(float(row[3]) * 1000) if isinstance(row[3], (float, int)) else None,
|
|
1873
|
+
"queue_wait_ms": round(float(row[4]) * 1000) if isinstance(row[4], (float, int)) else None,
|
|
1874
|
+
"provider_duration_ms": None, "validation_duration_ms": None, "external_wait_ms": None,
|
|
1875
|
+
"largest_phase": None, "producer_type": row[8], "repository": row[9],
|
|
1876
|
+
"provider": row[5], "model": row[6], "reasoning_profile": row[7],
|
|
1877
|
+
"phase_telemetry": "NOT_RECORDED",
|
|
1878
|
+
} for row in rows]
|
|
1879
|
+
durations = [row["total_duration_ms"] for row in run_rows if isinstance(row["total_duration_ms"], int)]
|
|
1880
|
+
waits = [row["queue_wait_ms"] for row in run_rows if isinstance(row["queue_wait_ms"], int)]
|
|
1881
|
+
def aggregate(values: list[int]) -> dict[str, int] | None:
|
|
1882
|
+
return {"average_ms": round(sum(values) / len(values)), "median_ms": sorted(values)[len(values) // 2], "total_ms": sum(values), "runs": len(values)} if values else None
|
|
1883
|
+
return {
|
|
1884
|
+
"date": execution_date, "timezone": "UTC", "runs": run_rows, "phases": [], "phase_telemetry_available": False,
|
|
1885
|
+
"summary": {"executions": len(run_rows), "completed": sum(row["status"] == "COMPLETE" for row in run_rows),
|
|
1886
|
+
"blocked": sum(row["status"] == "BLOCKED" for row in run_rows), "failed": sum(row["status"] == "FAILED" for row in run_rows),
|
|
1887
|
+
"total_wall_time": aggregate(durations), "queue_wait": aggregate(waits),
|
|
1888
|
+
"active_processing_time": None, "provider_execution": None, "validation": None, "external_wait": None, "overhead": None,
|
|
1889
|
+
"report_generation": None, "evidence_persistence": None},
|
|
1890
|
+
"bottlenecks": {"longest_average_phase": None, "largest_accumulated_phase": None, "top_time_consumers": [], "shares": {}},
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _central_console_report(data_root: Path, project_id: str, run_id: str) -> bytes | None:
|
|
1895
|
+
"""Read one CENTRAL-indexed immutable report with project authorization."""
|
|
1896
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1897
|
+
row = connection.execute(
|
|
1898
|
+
"""SELECT h.report_path FROM prompt_execution_history AS h
|
|
1899
|
+
JOIN ep_parity_lifecycle_dispatches AS d ON d.run_id=h.run_id
|
|
1900
|
+
WHERE d.project_id=? AND h.run_id=?""",
|
|
1901
|
+
(project_id, run_id),
|
|
1902
|
+
).fetchone()
|
|
1903
|
+
if row is None or not isinstance(row[0], str) or not row[0].startswith("CENTRAL:"):
|
|
1904
|
+
return None
|
|
1905
|
+
candidate = (data_root / "artifacts" / row[0].removeprefix("CENTRAL:")).resolve()
|
|
1906
|
+
try:
|
|
1907
|
+
candidate.relative_to((data_root / "artifacts").resolve())
|
|
1908
|
+
return candidate.read_bytes() if candidate.is_file() else None
|
|
1909
|
+
except (OSError, ValueError):
|
|
1910
|
+
return None
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
def _central_console_chat_history(data_root: Path, project_id: str, run_id: str) -> list[dict[str, object]] | None:
|
|
1914
|
+
"""Return a project-authorized CENTRAL transcript; no root fallback exists."""
|
|
1915
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
1916
|
+
belongs = connection.execute(
|
|
1917
|
+
"SELECT 1 FROM ep_parity_lifecycle_dispatches WHERE project_id=? AND run_id=?",
|
|
1918
|
+
(project_id, run_id),
|
|
1919
|
+
).fetchone()
|
|
1920
|
+
if belongs is None:
|
|
1921
|
+
return None
|
|
1922
|
+
rows = connection.execute(
|
|
1923
|
+
"SELECT role,content,model,created_at FROM execution_chat_messages WHERE run_id=? ORDER BY id",
|
|
1924
|
+
(run_id,),
|
|
1925
|
+
).fetchall()
|
|
1926
|
+
return [{"role": str(role), "content": str(content), "model": model, "created_at": str(created_at)}
|
|
1927
|
+
for role, content, model, created_at in rows]
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
@dataclass(frozen=True)
|
|
1931
|
+
class _CentralLogQuery:
|
|
1932
|
+
page: int
|
|
1933
|
+
page_size: int
|
|
1934
|
+
start_at: str
|
|
1935
|
+
end_at: str
|
|
1936
|
+
inclusive_end: bool
|
|
1937
|
+
search: str
|
|
1938
|
+
level: str
|
|
1939
|
+
events: tuple[str, ...]
|
|
1940
|
+
sort_key: str
|
|
1941
|
+
direction: str
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def _parse_central_log_query(values: dict[str, list[str]] | None) -> _CentralLogQuery:
|
|
1945
|
+
"""Validate the public query once, before composing any CENTRAL SQL."""
|
|
1946
|
+
query = values or {}
|
|
1947
|
+
|
|
1948
|
+
def first(name: str, default: str = "") -> str:
|
|
1949
|
+
return str((query.get(name) or [default])[0])
|
|
1950
|
+
|
|
1951
|
+
try:
|
|
1952
|
+
page = int(first("page", "1"))
|
|
1953
|
+
page_size = int(first("page_size", "50"))
|
|
1954
|
+
except ValueError as error:
|
|
1955
|
+
raise ValueError("Invalid component-log pagination.") from error
|
|
1956
|
+
if page < 1 or not 1 <= page_size <= MAX_COMPONENT_LOG_PAGE_SIZE:
|
|
1957
|
+
raise ValueError("Invalid component-log pagination.")
|
|
1958
|
+
level, search = first("level").upper().strip(), first("search").strip()
|
|
1959
|
+
if level and level not in VALID_LEVELS:
|
|
1960
|
+
raise ValueError("Invalid component-log level.")
|
|
1961
|
+
if len(search) > 160:
|
|
1962
|
+
raise ValueError("Component-log search is too long.")
|
|
1963
|
+
events = tuple(sorted({value.strip() for value in query.get("event", []) if value.strip()}))
|
|
1964
|
+
if len(events) > 50 or any(len(event) > 160 for event in events):
|
|
1965
|
+
raise ValueError("Invalid component-log event filter.")
|
|
1966
|
+
sort_key, direction = first("sort", "timestamp"), first("direction", "desc").lower()
|
|
1967
|
+
if sort_key not in _CENTRAL_LOG_SORT_COLUMNS or direction not in {"asc", "desc"}:
|
|
1968
|
+
raise ValueError("Invalid component-log sort.")
|
|
1969
|
+
return _CentralLogQuery(
|
|
1970
|
+
page=page,
|
|
1971
|
+
page_size=page_size,
|
|
1972
|
+
start_at=first("start").strip(),
|
|
1973
|
+
end_at=first("end").strip(),
|
|
1974
|
+
inclusive_end=first("inclusive_end") == "1",
|
|
1975
|
+
search=search,
|
|
1976
|
+
level=level,
|
|
1977
|
+
events=events,
|
|
1978
|
+
sort_key=sort_key,
|
|
1979
|
+
direction=direction,
|
|
1980
|
+
)
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
def _central_log_components(component: str) -> tuple[frozenset[str], tuple[str, ...]] | None:
|
|
1984
|
+
"""Resolve only canonical component identities without project context."""
|
|
1985
|
+
if component == "all":
|
|
1986
|
+
selected = PLATFORM_COMPONENT_IDS
|
|
1987
|
+
elif component in PLATFORM_COMPONENT_IDS:
|
|
1988
|
+
selected = frozenset({component})
|
|
1989
|
+
else:
|
|
1990
|
+
return None
|
|
1991
|
+
return selected, tuple(selected)
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
def _central_console_component_logs(
|
|
1995
|
+
data_root: Path,
|
|
1996
|
+
component: str,
|
|
1997
|
+
query: dict[str, list[str]] | None = None,
|
|
1998
|
+
*,
|
|
1999
|
+
export_all: bool = False,
|
|
2000
|
+
) -> dict[str, object] | None:
|
|
2001
|
+
"""Read one filtered, sorted CENTRAL log page before it reaches the Console."""
|
|
2002
|
+
selection = _central_log_components(component)
|
|
2003
|
+
if selection is None:
|
|
2004
|
+
return None
|
|
2005
|
+
_, stored_components = selection
|
|
2006
|
+
filters = _parse_central_log_query(query)
|
|
2007
|
+
clauses = ["component IN (" + ",".join("?" for _ in stored_components) + ")"]
|
|
2008
|
+
parameters: list[object] = list(stored_components)
|
|
2009
|
+
if filters.start_at:
|
|
2010
|
+
clauses.append("created_at >= ?")
|
|
2011
|
+
parameters.append(filters.start_at)
|
|
2012
|
+
if filters.end_at:
|
|
2013
|
+
clauses.append("created_at <= ?" if filters.inclusive_end else "created_at < ?")
|
|
2014
|
+
parameters.append(filters.end_at)
|
|
2015
|
+
if filters.search:
|
|
2016
|
+
escaped = filters.search.lower().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
2017
|
+
clauses.append("LOWER(payload) LIKE ? ESCAPE '\\'")
|
|
2018
|
+
parameters.append(f"%{escaped}%")
|
|
2019
|
+
if filters.level in LOG_LEVELS_AT_OR_ABOVE:
|
|
2020
|
+
levels = LOG_LEVELS_AT_OR_ABOVE[filters.level]
|
|
2021
|
+
clauses.append("json_extract(payload, '$.level') IN (" + ",".join("?" for _ in levels) + ")")
|
|
2022
|
+
parameters.extend(levels)
|
|
2023
|
+
event_option_clauses, event_option_parameters = list(clauses), list(parameters)
|
|
2024
|
+
if filters.events:
|
|
2025
|
+
clauses.append("json_extract(payload, '$.event') IN (" + ",".join("?" for _ in filters.events) + ")")
|
|
2026
|
+
parameters.extend(filters.events)
|
|
2027
|
+
where = " WHERE " + " AND ".join(clauses)
|
|
2028
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
2029
|
+
total = int(connection.execute("SELECT COUNT(*) FROM engineering_component_logs" + where, parameters).fetchone()[0])
|
|
2030
|
+
rows = connection.execute(
|
|
2031
|
+
"SELECT id,component,payload,created_at FROM engineering_component_logs" + where
|
|
2032
|
+
+ f" ORDER BY {_CENTRAL_LOG_SORT_COLUMNS[filters.sort_key]} {filters.direction.upper()}, id {filters.direction.upper()} LIMIT ? OFFSET ?",
|
|
2033
|
+
[
|
|
2034
|
+
*parameters,
|
|
2035
|
+
5000 if export_all else filters.page_size,
|
|
2036
|
+
0 if export_all else (filters.page - 1) * filters.page_size,
|
|
2037
|
+
],
|
|
2038
|
+
).fetchall()
|
|
2039
|
+
event_rows = connection.execute(
|
|
2040
|
+
"SELECT DISTINCT json_extract(payload, '$.event') FROM engineering_component_logs WHERE "
|
|
2041
|
+
+ " AND ".join(event_option_clauses)
|
|
2042
|
+
+ " AND json_extract(payload, '$.event') IS NOT NULL ORDER BY 1 LIMIT 500",
|
|
2043
|
+
event_option_parameters,
|
|
2044
|
+
).fetchall()
|
|
2045
|
+
entries: list[dict[str, object]] = []
|
|
2046
|
+
for identifier, stored_component, payload, created_at in rows:
|
|
2047
|
+
try:
|
|
2048
|
+
decoded = json.loads(str(payload))
|
|
2049
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
2050
|
+
decoded = {"event": "malformed_central_log"}
|
|
2051
|
+
record = decoded if isinstance(decoded, dict) else {}
|
|
2052
|
+
entries.append({"line": int(identifier), "timestamp": str(created_at), **record, "component": str(stored_component)})
|
|
2053
|
+
return {
|
|
2054
|
+
"scope": "PLATFORM", "component": component, "entries": entries,
|
|
2055
|
+
"page": filters.page, "page_size": filters.page_size, "total": total,
|
|
2056
|
+
"events": [str(row[0]) for row in event_rows if row[0]],
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def _clear_central_console_component_logs(data_root: Path, component: str) -> dict[str, object] | None:
|
|
2061
|
+
"""Delete only the explicitly selected CENTRAL component-log projection."""
|
|
2062
|
+
if component == "all":
|
|
2063
|
+
selected = PLATFORM_COMPONENT_IDS
|
|
2064
|
+
elif component in PLATFORM_COMPONENT_IDS:
|
|
2065
|
+
selected = frozenset({component})
|
|
2066
|
+
else:
|
|
2067
|
+
return None
|
|
2068
|
+
stored = tuple(selected)
|
|
2069
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
2070
|
+
cursor = connection.execute(
|
|
2071
|
+
"DELETE FROM engineering_component_logs WHERE component IN (" + ",".join("?" for _ in stored) + ")",
|
|
2072
|
+
stored,
|
|
2073
|
+
)
|
|
2074
|
+
return {"scope": "PLATFORM", "component": component, "deleted": int(cursor.rowcount)}
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def _central_console_configuration(data_root: Path) -> dict[str, object]:
|
|
2078
|
+
"""Expose only configuration already owned by CENTRAL in this phase."""
|
|
2079
|
+
return {
|
|
2080
|
+
"scope": "PLATFORM",
|
|
2081
|
+
**central_database.maintenance_configuration(data_root),
|
|
2082
|
+
**central_database.capacity_configuration(data_root),
|
|
2083
|
+
**central_database.console_interval_configuration(data_root),
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
def _central_provider_readiness(data_root: Path) -> dict[str, dict[str, object]]:
|
|
2088
|
+
"""Project-independent, token-free authentication readiness for CENTRAL."""
|
|
2089
|
+
statuses = provider_readiness.host_status(data_root)
|
|
2090
|
+
runtime = provider_readiness.runtime_details(data_root)
|
|
2091
|
+
return {
|
|
2092
|
+
provider: {**value, **runtime.get(provider, {}), "scope": "PLATFORM"}
|
|
2093
|
+
for provider, value in statuses.items()
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
def _audit_dashboard_provider_action(
|
|
2098
|
+
data_root: Path, provider: str, action: str, outcome: str, *, level: int = logging.INFO,
|
|
2099
|
+
) -> None:
|
|
2100
|
+
"""Persist a secret-free audit fact for one host-wide Console action."""
|
|
2101
|
+
log_event(
|
|
2102
|
+
component_logger(
|
|
2103
|
+
data_root, "operations_console", central_database=data_root / SERVER_DATABASE_FILENAME,
|
|
2104
|
+
),
|
|
2105
|
+
level,
|
|
2106
|
+
f"provider_action_{outcome.lower()}",
|
|
2107
|
+
context={
|
|
2108
|
+
"provider": provider,
|
|
2109
|
+
"provider_action": action,
|
|
2110
|
+
"provider_action_source": "DASHBOARD",
|
|
2111
|
+
"audit_outcome": outcome,
|
|
2112
|
+
},
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _central_provider_repair(data_root: Path, payload: object) -> None:
|
|
2117
|
+
"""Start one validated host-wide provider action without a checkout."""
|
|
2118
|
+
if not isinstance(payload, dict) or set(payload) != {"provider", "action"}:
|
|
2119
|
+
raise ValueError("Invalid provider repair request.")
|
|
2120
|
+
provider, action = str(payload["provider"]), str(payload["action"])
|
|
2121
|
+
if provider not in {"CODEX", "GITHUB"} or action not in {"login", "install"}:
|
|
2122
|
+
raise ValueError("Invalid provider repair request.")
|
|
2123
|
+
_audit_dashboard_provider_action(data_root, provider, action, "REQUESTED")
|
|
2124
|
+
try:
|
|
2125
|
+
readiness = _central_provider_readiness(data_root)
|
|
2126
|
+
state = str(readiness[provider.lower()]["state"])
|
|
2127
|
+
if (action == "login" and state != "AUTH_REQUIRED") or (action == "install" and state != "UNAVAILABLE"):
|
|
2128
|
+
raise ValueError("Provider is not ready for the requested repair.")
|
|
2129
|
+
if action == "login":
|
|
2130
|
+
_start_provider_login(data_root, provider)
|
|
2131
|
+
_audit_dashboard_provider_action(data_root, provider, action, "STARTED")
|
|
2132
|
+
else:
|
|
2133
|
+
_install_provider(data_root, provider)
|
|
2134
|
+
_audit_dashboard_provider_action(data_root, provider, action, "COMPLETED")
|
|
2135
|
+
except (OSError, RuntimeError, ValueError):
|
|
2136
|
+
_audit_dashboard_provider_action(data_root, provider, action, "FAILED", level=logging.WARNING)
|
|
2137
|
+
raise
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
def _central_provider_logout(data_root: Path, payload: object) -> None:
|
|
2141
|
+
"""Remove one verified host-wide provider session through CENTRAL.
|
|
2142
|
+
|
|
2143
|
+
The Console never owns provider credentials. It can only request this
|
|
2144
|
+
narrowly validated host operation while the provider is known ready, so a
|
|
2145
|
+
stale or fabricated UI request cannot be delegated to a checkout-bound
|
|
2146
|
+
legacy handler.
|
|
2147
|
+
"""
|
|
2148
|
+
if not isinstance(payload, dict) or set(payload) != {"provider"}:
|
|
2149
|
+
raise ValueError("Invalid provider logout request.")
|
|
2150
|
+
provider = str(payload["provider"])
|
|
2151
|
+
if provider not in {"CODEX", "GITHUB"}:
|
|
2152
|
+
raise ValueError("Invalid provider logout request.")
|
|
2153
|
+
_audit_dashboard_provider_action(data_root, provider, "logout", "REQUESTED")
|
|
2154
|
+
try:
|
|
2155
|
+
readiness = _central_provider_readiness(data_root)
|
|
2156
|
+
if str(readiness[provider.lower()]["state"]) != "READY":
|
|
2157
|
+
raise ValueError("Provider is not ready for logout.")
|
|
2158
|
+
_logout_provider(data_root, provider)
|
|
2159
|
+
_audit_dashboard_provider_action(data_root, provider, "logout", "COMPLETED")
|
|
2160
|
+
except (OSError, RuntimeError, ValueError):
|
|
2161
|
+
_audit_dashboard_provider_action(data_root, provider, "logout", "FAILED", level=logging.WARNING)
|
|
2162
|
+
raise
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
def _with_console_queue(payload: bytes, *, queue: dict[str, object], data_root: Path) -> bytes:
|
|
2166
|
+
"""Overlay CENTRAL-only queue and provider evidence onto legacy payloads."""
|
|
2167
|
+
try:
|
|
2168
|
+
decoded = json.loads(payload)
|
|
2169
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
2170
|
+
return payload
|
|
2171
|
+
if not isinstance(decoded, dict):
|
|
2172
|
+
return payload
|
|
2173
|
+
handling = queue.get("operator_handling")
|
|
2174
|
+
if isinstance(handling, dict) and isinstance(decoded.get("runs"), list):
|
|
2175
|
+
for run in decoded["runs"]:
|
|
2176
|
+
if isinstance(run, dict) and handling.get(run.get("run_id")) == "DISMISSED":
|
|
2177
|
+
run["dismissed"] = True
|
|
2178
|
+
run["handling_state"] = "DISMISSED"
|
|
2179
|
+
status_payload = decoded.get("status")
|
|
2180
|
+
if isinstance(status_payload, dict):
|
|
2181
|
+
decoded["status"] = {**status_payload, **queue}
|
|
2182
|
+
else:
|
|
2183
|
+
decoded = {**decoded, **queue}
|
|
2184
|
+
rate_limits = decoded.get("rate_limits")
|
|
2185
|
+
if isinstance(rate_limits, dict):
|
|
2186
|
+
provider = rate_limits.get("provider")
|
|
2187
|
+
remaining = _remaining_rate_limit_capacity(rate_limits)
|
|
2188
|
+
if isinstance(provider, str) and remaining is not None:
|
|
2189
|
+
decoded["ai_capacity_history"] = central_database.record_provider_capacity(
|
|
2190
|
+
data_root, provider=provider, remaining_percent=remaining,
|
|
2191
|
+
)
|
|
2192
|
+
decoded["capacity_scope"] = "EP"
|
|
2193
|
+
decoded["capacity_configuration"] = central_database.capacity_configuration(data_root)
|
|
2194
|
+
return json.dumps(decoded, separators=(",", ":")).encode("utf-8")
|
|
2195
|
+
|
|
2196
|
+
|
|
2197
|
+
def _provider_capacity_projection(data_root: Path) -> dict[str, object]:
|
|
2198
|
+
"""Read the account-owned quota once and project it from CENTRAL."""
|
|
2199
|
+
try:
|
|
2200
|
+
payload = json.loads(_codex_rate_limits())
|
|
2201
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
2202
|
+
payload = {}
|
|
2203
|
+
if not isinstance(payload, dict):
|
|
2204
|
+
payload = {}
|
|
2205
|
+
provider = payload.get("provider")
|
|
2206
|
+
remaining = _remaining_rate_limit_capacity(payload)
|
|
2207
|
+
history: list[dict[str, object]] = []
|
|
2208
|
+
if isinstance(provider, str) and remaining is not None:
|
|
2209
|
+
history = central_database.record_provider_capacity(
|
|
2210
|
+
data_root, provider=provider, remaining_percent=remaining,
|
|
2211
|
+
)
|
|
2212
|
+
return {
|
|
2213
|
+
"rate_limits": payload,
|
|
2214
|
+
"ai_capacity_history": history,
|
|
2215
|
+
"scope": "EP",
|
|
2216
|
+
"configuration": central_database.capacity_configuration(data_root),
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
def _console_project_options(project_id: str | None, projects: list[dict[str, str]]) -> str:
|
|
2221
|
+
"""Render the safe empty choice plus registered CENTRAL identities."""
|
|
2222
|
+
empty = '<option value=""' + (" selected" if project_id is None else "") + '><geen></option>'
|
|
2223
|
+
return empty + "".join(
|
|
2224
|
+
f'<option value="{escape(item["project_id"], quote=True)}"'
|
|
2225
|
+
f'{" selected" if item["project_id"] == project_id else ""}>'
|
|
2226
|
+
f'{escape(item["project_id"])}</option>'
|
|
2227
|
+
for item in projects
|
|
2228
|
+
)
|
|
2229
|
+
|
|
2230
|
+
|
|
2231
|
+
def _central_database_section(data_root: Path) -> str:
|
|
2232
|
+
"""Render the one installation-owned EP database panel for Configuration."""
|
|
2233
|
+
details = central_database.details(data_root)
|
|
2234
|
+
interval = central_database.maintenance_configuration(data_root)["interval_seconds"]
|
|
2235
|
+
size = f"{int(details['size_bytes']) / 1_000_000:.2f}".replace(".", ",") + " MB"
|
|
2236
|
+
options = "".join(
|
|
2237
|
+
f'<option value="{value}"{" selected" if value == interval else ""}>{label}</option>'
|
|
2238
|
+
for value, label in ((60, "1 minuut"), (3600, "1 uur"), (86400, "1 dag"), (604800, "1 week"))
|
|
2239
|
+
)
|
|
2240
|
+
return (
|
|
2241
|
+
'<section class="configuration-central-database" aria-labelledby="centralDatabaseHeading">'
|
|
2242
|
+
'<header class="configuration-central-database__header">'
|
|
2243
|
+
'<div><h2 id="centralDatabaseHeading" data-i18n="configuration.ep_database">EP-database</h2>'
|
|
2244
|
+
'<p data-i18n="configuration.ep_database_description">Platformbrede opslag voor projecten, uitvoeringen en configuratie.</p></div>'
|
|
2245
|
+
'<div class="configuration-central-database__actions"><a class="configuration-central-database__export" href="/api/central-data/export" download '
|
|
2246
|
+
'data-i18n="configuration.central_data_export" data-i18n-aria-label="configuration.central_data_export" '
|
|
2247
|
+
'aria-label="Exporteer platformgegevens">Exporteer platformgegevens</a><button class="configuration-central-database__relocate" id="centralDataImport" type="button" data-i18n="configuration.central_data_import">Importeer platformgegevens</button></div></header>'
|
|
2248
|
+
'<dl class="configuration-central-database__facts">'
|
|
2249
|
+
f'<div class="configuration-central-database__location"><dt class="label" data-i18n="configuration.platform_data_location">Platformgegevenslocatie</dt><dd class="configuration-central-database__location-value"><button class="configuration-central-database__location-link local-folder-link" type="button" data-local-path="{escape(str(data_root.resolve()))}">{escape(str(data_root.resolve()))}</button><button class="configuration-central-database__relocate" id="centralDatabaseRelocate" type="button" data-i18n="configuration.relocate_platform_data">Verplaats platformgegevens</button></dd></div>'
|
|
2250
|
+
f'<div><dt class="label" data-i18n="configuration.database_size">Databasegrootte</dt><dd>{size}</dd></div>'
|
|
2251
|
+
f'<div><dt class="label" data-i18n="configuration.schema_version">Schema-versie</dt><dd>{details["schema_version"]}</dd></div>'
|
|
2252
|
+
f'<div><dt class="label" data-i18n="configuration.integrity">Integriteit</dt><dd data-i18n="configuration.database_integrity.{details["integrity"]}">{details["integrity"]}</dd></div>'
|
|
2253
|
+
'</dl>'
|
|
2254
|
+
'<div class="configuration-central-database__maintenance">'
|
|
2255
|
+
'<div><span class="label" id="centralDatabaseMaintenanceLabel" data-i18n="configuration.ep_database_maintenance">Databaseonderhoud</span>'
|
|
2256
|
+
'<p id="centralDatabaseMaintenanceHelp" data-i18n="configuration.ep_database_maintenance_help">Optimaliseert de EP-database wanneer geen uitvoering actief is.</p></div>'
|
|
2257
|
+
f'<select id="centralDatabaseMaintenanceInterval" aria-labelledby="centralDatabaseMaintenanceLabel" aria-describedby="centralDatabaseMaintenanceHelp centralDatabaseMaintenanceStatus" data-saved-value="{interval}">{options}</select>'
|
|
2258
|
+
'</div>'
|
|
2259
|
+
'<p id="centralDatabaseMaintenanceStatus" role="status" aria-live="polite"></p></section>'
|
|
2260
|
+
f'<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation installation-relocation-modal" id="centralDatabaseRelocateModal"><section class="dashboard-modal-shell__panel"><header class="dashboard-modal-shell__header"><h2 data-modal-glyph="relocate" data-i18n="configuration.relocate_platform_data">Verplaats platformgegevens</h2><button class="dashboard-modal-shell__close" type="button" aria-label="Close" data-close-relocation="centralDatabaseRelocateModal">×</button></header><p data-i18n="configuration.relocate_platform_data_help">Verplaats alle platformgegevens als één geheel. De server stopt veilig en start daarna opnieuw.</p><dl class="installation-relocation-modal__locations"><div><dt data-i18n="configuration.current_folder">Huidige map</dt><dd><button class="local-folder-link" type="button" data-local-path="{escape(str(data_root.resolve()))}">{escape(str(data_root.resolve()))}</button></dd></div><div id="centralDatabaseRelocateDestination" hidden><dt data-i18n="configuration.new_folder">Nieuwe map</dt><dd id="centralDatabaseRelocateDestinationValue"></dd></div></dl><input id="centralDatabaseRelocateDirectory" type="hidden"><div class="dashboard-modal-shell__actions"><button class="dashboard-modal-shell__action" id="centralDatabaseRelocateBrowse" type="button" data-i18n="configuration.choose_folder">Kies map</button><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="centralDatabaseRelocateSave" type="button" disabled data-i18n="configuration.relocate">Verplaatsen</button></div><p id="centralDatabaseRelocateStatus" role="status"></p></section></dialog>'
|
|
2261
|
+
'<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation installation-relocation-modal" id="centralDataImportModal"><section class="dashboard-modal-shell__panel"><header class="dashboard-modal-shell__header"><h2 data-modal-glyph="relocate" data-i18n="configuration.central_data_import">Importeer platformgegevens</h2><button class="dashboard-modal-shell__close" type="button" aria-label="Close" data-close-central-import>×</button></header><p data-i18n="configuration.central_data_import_help">Kies een eerder geëxporteerd ZIP-bestand. Alle huidige platformgegevens worden vervangen.</p><input id="centralDataImportFile" type="file" accept="application/zip,.zip"><p class="installation-relocation-modal__warning" data-i18n="configuration.central_data_import_warning">Dit vervangt de volledige huidige platformstatus.</p><div class="dashboard-modal-shell__actions"><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="centralDataImportConfirm" type="button" disabled data-i18n="configuration.central_data_import_confirm">Importeren en herstarten</button></div><p id="centralDataImportStatus" role="status"></p></section></dialog>'
|
|
2262
|
+
)
|
|
2263
|
+
|
|
2264
|
+
|
|
2265
|
+
def _central_database_script() -> str:
|
|
2266
|
+
"""Bind CENTRAL maintenance plus whole-state transfer controls."""
|
|
2267
|
+
return '''for(const id of ['centralDatabaseRelocateModal','centralDataImportModal','fileInboxRelocateModal']){const dialog=document.getElementById(id);if(dialog&&dialog.parentElement!==document.body)document.body.append(dialog)}const maintenance=document.getElementById('centralDatabaseMaintenanceInterval'),maintenanceStatus=document.getElementById('centralDatabaseMaintenanceStatus'),translate=window.__engineeringPlatformDashboardTranslate;if(maintenance)maintenance.addEventListener('change',async()=>{const previous=maintenance.dataset.savedValue||maintenance.value,requested=Number(maintenance.value);maintenance.disabled=true;try{const response=await fetch('/api/central-database/configuration',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({interval_seconds:requested})});const result=response.ok?await response.json():null;if(!result||Number(result.interval_seconds)!==requested)throw Error();maintenance.dataset.savedValue=String(requested);if(maintenanceStatus)maintenanceStatus.textContent=translate('configuration.ep_database_maintenance_saved')}catch{maintenance.value=previous;if(maintenanceStatus)maintenanceStatus.textContent=translate('configuration.ep_database_maintenance_failed')}finally{maintenance.disabled=false}});const modal=document.getElementById('centralDatabaseRelocateModal'),open=document.getElementById('centralDatabaseRelocate'),input=document.getElementById('centralDatabaseRelocateDirectory'),destination=document.getElementById('centralDatabaseRelocateDestination'),destinationValue=document.getElementById('centralDatabaseRelocateDestinationValue'),save=document.getElementById('centralDatabaseRelocateSave'),status=document.getElementById('centralDatabaseRelocateStatus'),showDestination=value=>{input.value=value;destinationValue.replaceChildren(window.__engineeringPlatformLocalFilesystemLink(value));destination.hidden=false;save.disabled=false;};open?.addEventListener('click',()=>modal.showModal());modal?.querySelector('[data-close-relocation]')?.addEventListener('click',()=>modal.close());document.getElementById('centralDatabaseRelocateBrowse')?.addEventListener('click',async()=>{const r=await fetch('/api/central-data/relocate/browse',{method:'POST'}),p=await r.json();if(p.value)showDestination(p.value);});save?.addEventListener('click',async()=>{const r=await fetch('/api/central-data/relocate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({directory:input.value})}),p=await r.json();status.textContent=r.ok?translate('configuration.relocation_restarting'):p.error||translate('configuration.relocation_failed');if(r.ok)setTimeout(()=>location.reload(),2500);});const importModal=document.getElementById('centralDataImportModal'),importOpen=document.getElementById('centralDataImport'),importFile=document.getElementById('centralDataImportFile'),importSave=document.getElementById('centralDataImportConfirm'),importStatus=document.getElementById('centralDataImportStatus');importOpen?.addEventListener('click',()=>importModal.showModal());importModal?.querySelector('[data-close-central-import]')?.addEventListener('click',()=>importModal.close());importFile?.addEventListener('change',()=>{importSave.disabled=!importFile.files?.length;});importSave?.addEventListener('click',async()=>{const file=importFile.files?.[0];if(!file)return;importSave.disabled=true;const r=await fetch('/api/central-data/import',{method:'POST',headers:{'Content-Type':'application/zip','X-EP-Central-Import-Confirmed':'true'},body:file}),p=await r.json();importStatus.textContent=r.ok?translate('configuration.relocation_restarting'):p.error||translate('configuration.relocation_failed');if(r.ok)setTimeout(()=>location.reload(),2500);else importSave.disabled=false;});'''
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
def _file_inbox_section(data_root: Path) -> str:
|
|
2271
|
+
"""Render File Inbox relocation with the same installation-owned affordance."""
|
|
2272
|
+
location = escape(str((data_root / FILE_INBOX_DIRECTORY).resolve()))
|
|
2273
|
+
return f'''<section class="configuration-central-database" aria-labelledby="fileInboxHeading"><header class="configuration-central-database__header"><div><h2 id="fileInboxHeading" data-i18n="configuration.file_inbox">File Inbox</h2><p data-i18n="configuration.file_inbox_relocation_help">Verplaats de File Inbox alleen wanneer deze leeg is.</p></div></header><dl class="configuration-central-database__facts"><div class="configuration-central-database__location"><dt class="label" data-i18n="configuration.file_inbox_location">File Inbox-locatie</dt><dd class="configuration-central-database__location-value"><button class="configuration-central-database__location-link local-folder-link" type="button" data-local-path="{location}">{location}</button><button class="configuration-central-database__relocate" id="fileInboxRelocate" type="button" data-i18n="configuration.relocate_file_inbox">Verplaats File Inbox</button></dd></div></dl></section><dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation installation-relocation-modal" id="fileInboxRelocateModal"><section class="dashboard-modal-shell__panel"><header class="dashboard-modal-shell__header"><h2 data-modal-glyph="relocate" data-i18n="configuration.relocate_file_inbox">Verplaats File Inbox</h2><button class="dashboard-modal-shell__close" type="button" aria-label="Close" data-close-relocation="fileInboxRelocateModal">×</button></header><p data-i18n="configuration.file_inbox_relocation_help">Verplaats de File Inbox alleen wanneer deze leeg is.</p><dl class="installation-relocation-modal__locations"><div><dt data-i18n="configuration.current_folder">Huidige map</dt><dd><button class="local-folder-link" type="button" data-local-path="{location}">{location}</button></dd></div><div id="fileInboxRelocateDestination" hidden><dt data-i18n="configuration.new_folder">Nieuwe map</dt><dd id="fileInboxRelocateDestinationValue"></dd></div></dl><input id="fileInboxRelocateDirectory" type="hidden"><div class="dashboard-modal-shell__actions"><button class="dashboard-modal-shell__action" id="fileInboxRelocateBrowse" type="button" data-i18n="configuration.choose_folder">Kies map</button><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="fileInboxRelocateSave" type="button" disabled data-i18n="configuration.relocate">Verplaatsen</button></div><p id="fileInboxRelocateStatus" role="status"></p></section></dialog>'''
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def _file_inbox_relocation_script() -> str:
|
|
2277
|
+
return '''const inboxModal=document.getElementById('fileInboxRelocateModal'),inboxOpen=document.getElementById('fileInboxRelocate'),inboxInput=document.getElementById('fileInboxRelocateDirectory'),inboxDestination=document.getElementById('fileInboxRelocateDestination'),inboxDestinationValue=document.getElementById('fileInboxRelocateDestinationValue'),inboxSave=document.getElementById('fileInboxRelocateSave'),inboxStatus=document.getElementById('fileInboxRelocateStatus'),showInboxDestination=value=>{inboxInput.value=value;inboxDestinationValue.replaceChildren(window.__engineeringPlatformLocalFilesystemLink(value));inboxDestination.hidden=false;inboxSave.disabled=false;};inboxOpen?.addEventListener('click',()=>inboxModal.showModal());inboxModal?.querySelector('[data-close-relocation]')?.addEventListener('click',()=>inboxModal.close());document.getElementById('fileInboxRelocateBrowse')?.addEventListener('click',async()=>{const r=await fetch('/api/configuration/file-inbox/relocate/browse',{method:'POST'}),p=await r.json();if(p.value)showInboxDestination(p.value);});inboxSave?.addEventListener('click',async()=>{const r=await fetch('/api/configuration/file-inbox/relocate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({directory:inboxInput.value})}),p=await r.json();inboxStatus.textContent=r.ok?window.__engineeringPlatformDashboardTranslate('configuration.relocation_restarting'):p.error||window.__engineeringPlatformDashboardTranslate('configuration.relocation_failed');if(r.ok)setTimeout(()=>location.reload(),2500);});'''
|
|
2278
|
+
|
|
2279
|
+
|
|
2280
|
+
def _console_project_boundary(project_id: str, options: str) -> str:
|
|
2281
|
+
"""Bind CENTRAL selector options and request scope to a dashboard document.
|
|
2282
|
+
|
|
2283
|
+
The historical dashboard initially renders its own selector. Replacing
|
|
2284
|
+
those options happens after the generic visual picker is initialized, so
|
|
2285
|
+
the explicit event is the boundary contract that keeps the two controls
|
|
2286
|
+
synchronized without exposing CENTRAL details to dashboard internals.
|
|
2287
|
+
"""
|
|
2288
|
+
return '''<script>
|
|
2289
|
+
(() => {
|
|
2290
|
+
const project = $PROJECT;
|
|
2291
|
+
const options = $OPTIONS;
|
|
2292
|
+
const nativeFetch = window.fetch.bind(window);
|
|
2293
|
+
window.fetch = (input, init = {}) => {
|
|
2294
|
+
const headers = new Headers(init.headers || (input instanceof Request ? input.headers : undefined));
|
|
2295
|
+
headers.set('X-Engineering-Platform-Project', project);
|
|
2296
|
+
return nativeFetch(input, { ...init, headers });
|
|
2297
|
+
};
|
|
2298
|
+
const NativeEventSource = window.EventSource;
|
|
2299
|
+
window.EventSource = function(url, config) {
|
|
2300
|
+
const target = new URL(url, window.location.href);
|
|
2301
|
+
target.searchParams.set('project', project);
|
|
2302
|
+
return new NativeEventSource(target, config);
|
|
2303
|
+
};
|
|
2304
|
+
window.EventSource.prototype = NativeEventSource.prototype;
|
|
2305
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
2306
|
+
const select = document.getElementById('dashboardProject');
|
|
2307
|
+
if (!select) return;
|
|
2308
|
+
select.innerHTML = options;
|
|
2309
|
+
select.value = project;
|
|
2310
|
+
select.dispatchEvent(new Event('dashboard-select-options-changed', { bubbles: true }));
|
|
2311
|
+
select.addEventListener('change', () => {
|
|
2312
|
+
const url = new URL(window.location.href);
|
|
2313
|
+
url.searchParams.set('project', select.value);
|
|
2314
|
+
window.location.assign(url);
|
|
2315
|
+
});
|
|
2316
|
+
$CENTRAL_DATABASE_SCRIPT
|
|
2317
|
+
});
|
|
2318
|
+
})();
|
|
2319
|
+
</script>'''.replace("$PROJECT", json.dumps(project_id)).replace("$OPTIONS", json.dumps(options)).replace(
|
|
2320
|
+
"$CENTRAL_DATABASE_SCRIPT", _central_database_script(),
|
|
2321
|
+
)
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
def _no_project_console_document(projects: list[dict[str, str]], data_root: Path) -> bytes:
|
|
2325
|
+
"""Render global Console controls without selecting project-owned data."""
|
|
2326
|
+
document = server_console_services.render_console_document(
|
|
2327
|
+
"EP Operations",
|
|
2328
|
+
workspace_id="none",
|
|
2329
|
+
project_name="<geen>",
|
|
2330
|
+
workspace_location="",
|
|
2331
|
+
configuration_inbox="",
|
|
2332
|
+
)
|
|
2333
|
+
options = _console_project_options(None, projects)
|
|
2334
|
+
selector = f'''<label class="dashboard-project" for="dashboardProject"><span data-i18n="project.label"></span><select id="dashboardProject" data-i18n-aria-label="project.label">{options}</select></label>'''
|
|
2335
|
+
boundary = '''<script>window.ENGINEERING_PLATFORM_NO_PROJECT=true;(function(){const select=document.getElementById('dashboardProject');if(select)select.addEventListener('change',()=>{const url=new URL(window.location.href);if(select.value)url.searchParams.set('project',select.value);else url.searchParams.delete('project');window.location.assign(url)});$CENTRAL_DATABASE_SCRIPT})();</script>'''.replace("$CENTRAL_DATABASE_SCRIPT", _central_database_script())
|
|
2336
|
+
empty_state = '''<aside class="dashboard-status-banner dashboard-status-banner--no-project" id="noProjectSelected" role="status" aria-live="polite" data-testid="no-project-selected"><strong data-i18n="central.no_project_selected_title"></strong><span data-i18n="central.no_project_selected_body"></span><button class="no-project-selected__dismiss" id="noProjectSelectedDismiss" type="button" data-i18n-aria-label="action.close" data-i18n-title="action.close"><span aria-hidden="true">×</span></button></aside>'''
|
|
2337
|
+
scoped_style = '''<style>
|
|
2338
|
+
body[data-project-id="none"] #queueItems,
|
|
2339
|
+
body[data-project-id="none"] #promptHistory,
|
|
2340
|
+
body[data-project-id="none"] #currentRun,
|
|
2341
|
+
body[data-project-id="none"] #technicalDetails,
|
|
2342
|
+
body[data-project-id="none"] #workspaceCard { display: none !important; }
|
|
2343
|
+
</style>'''
|
|
2344
|
+
document = re.sub(
|
|
2345
|
+
br'<body data-project-id="[^"]*" data-project-name="[^"]*">',
|
|
2346
|
+
b'<body data-project-id="none" data-project-name="<geen>">',
|
|
2347
|
+
document,
|
|
2348
|
+
count=1,
|
|
2349
|
+
)
|
|
2350
|
+
document = document.replace(
|
|
2351
|
+
b'<label class="dashboard-locale"',
|
|
2352
|
+
selector.encode("utf-8") + b'<label class="dashboard-locale"',
|
|
2353
|
+
1,
|
|
2354
|
+
)
|
|
2355
|
+
document = document.replace(b'<pre></pre>', b'<pre data-i18n="format.not_available"></pre>', 1)
|
|
2356
|
+
# Keep the unscoped explanation in the sticky header. It is operational
|
|
2357
|
+
# context, not a project card that should scroll away with the dashboard.
|
|
2358
|
+
document = document.replace(
|
|
2359
|
+
b'<aside class="dashboard-status-banner dashboard-status-banner--usage-limit"',
|
|
2360
|
+
empty_state.encode("utf-8")
|
|
2361
|
+
+ b'<aside class="dashboard-status-banner dashboard-status-banner--usage-limit"',
|
|
2362
|
+
1,
|
|
2363
|
+
)
|
|
2364
|
+
document = document.replace(
|
|
2365
|
+
b'<main class="dashboard-grid"',
|
|
2366
|
+
boundary.encode("utf-8") + b'<main class="dashboard-grid"',
|
|
2367
|
+
1,
|
|
2368
|
+
)
|
|
2369
|
+
document = document.replace(
|
|
2370
|
+
b'<p class="category-description" data-i18n="description.configuration"></p>',
|
|
2371
|
+
b'<p class="category-description" data-i18n="description.configuration"></p>' + _central_database_section(data_root).encode("utf-8"),
|
|
2372
|
+
1,
|
|
2373
|
+
)
|
|
2374
|
+
return document.replace(b"</head>", scoped_style.encode("utf-8") + b"</head>", 1)
|
|
2375
|
+
|
|
2376
|
+
|
|
2377
|
+
def _selected_project_console_document(project_id: str, projects: list[dict[str, str]], data_root: Path) -> bytes:
|
|
2378
|
+
"""Render the installed Console shell without loading a project checkout."""
|
|
2379
|
+
document = server_console_services.render_console_document(
|
|
2380
|
+
"EP Operations", workspace_id=project_id, project_name=project_id,
|
|
2381
|
+
workspace_location="",
|
|
2382
|
+
configuration_inbox="",
|
|
2383
|
+
)
|
|
2384
|
+
options = _console_project_options(project_id, projects)
|
|
2385
|
+
selector = f'''<label class="dashboard-project" for="dashboardProject"><span data-i18n="project.label"></span><select id="dashboardProject" data-i18n-aria-label="project.label">{options}</select></label>'''
|
|
2386
|
+
document = document.replace(
|
|
2387
|
+
b'<label class="dashboard-locale"', selector.encode("utf-8") + b'<label class="dashboard-locale"', 1,
|
|
2388
|
+
)
|
|
2389
|
+
document = document.replace(b'<pre></pre>', b'<pre data-i18n="central.project_workspace_not_authority"></pre>', 1)
|
|
2390
|
+
document = document.replace(
|
|
2391
|
+
b'<p class="category-description" data-i18n="description.configuration"></p>',
|
|
2392
|
+
b'<p class="category-description" data-i18n="description.configuration"></p>' + _central_database_section(data_root).encode("utf-8"), 1,
|
|
2393
|
+
)
|
|
2394
|
+
# A project selection establishes CENTRAL scope; it is not a local
|
|
2395
|
+
# workspace binding. The generic historical template still contains a
|
|
2396
|
+
# checkout/branch/worktree card, which has no authoritative Server data
|
|
2397
|
+
# and no supported action route. Execution-bound checkout evidence stays
|
|
2398
|
+
# in ``#executionContext`` and is deliberately not part of this removal.
|
|
2399
|
+
document = re.sub(
|
|
2400
|
+
br'<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation confirmation-modal" id="workspaceBranchMainResultModal".*?</dialog>\n',
|
|
2401
|
+
b"",
|
|
2402
|
+
document,
|
|
2403
|
+
count=1,
|
|
2404
|
+
flags=re.DOTALL,
|
|
2405
|
+
)
|
|
2406
|
+
document = re.sub(
|
|
2407
|
+
br'<details class="card card--context workspace-card" id="workspaceCard".*?</details>\n',
|
|
2408
|
+
b"",
|
|
2409
|
+
document,
|
|
2410
|
+
count=1,
|
|
2411
|
+
flags=re.DOTALL,
|
|
2412
|
+
)
|
|
2413
|
+
return document.replace(b"</main>", _console_project_boundary(project_id, options).encode("utf-8") + b"</main>", 1)
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
_CONSOLE_STATIC_ASSETS = {
|
|
2417
|
+
"/assets/dashboard.css": ("dashboard.css", "text/css; charset=utf-8"),
|
|
2418
|
+
"/assets/dashboard.js": ("dashboard.js", "text/javascript; charset=utf-8"),
|
|
2419
|
+
"/assets/dashboard_locales.mjs": ("dashboard_locales.mjs", "text/javascript; charset=utf-8"),
|
|
2420
|
+
"/assets/dashboard_status_store.mjs": ("dashboard_status_store.mjs", "text/javascript; charset=utf-8"),
|
|
2421
|
+
"/assets/operations-console/icon-dark.png": ("operations-console/icon-dark.png", "image/png"),
|
|
2422
|
+
"/assets/operations-console/icon-light.png": ("operations-console/icon-light.png", "image/png"),
|
|
2423
|
+
"/assets/operations-console/icon-transparent.png": ("operations-console/icon-transparent.png", "image/png"),
|
|
2424
|
+
"/assets/operations-console/apple-touch-icon-dark.png": (console_presentation.APP_ICON_DARK, "image/png"),
|
|
2425
|
+
"/assets/operations-console/apple-touch-icon-light.png": (console_presentation.APP_ICON_LIGHT, "image/png"),
|
|
2426
|
+
"/assets/operations-console/manifest.webmanifest": (console_presentation.WEB_MANIFEST, "application/manifest+json; charset=utf-8"),
|
|
2427
|
+
"/favicon.ico": (console_presentation.APP_ICON_DARK, "image/png"),
|
|
2428
|
+
"/apple-touch-icon.png": (console_presentation.APP_ICON_DARK, "image/png"),
|
|
2429
|
+
"/apple-touch-icon-precomposed.png": (console_presentation.APP_ICON_DARK, "image/png"),
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
|
|
2433
|
+
def _no_project_platform_projection(data_root: Path) -> dict[str, object]:
|
|
2434
|
+
"""Return a checkout-free platform projection for the ``<geen>`` view.
|
|
2435
|
+
|
|
2436
|
+
This deliberately has no project fallback. It uses only installed Server
|
|
2437
|
+
state and CENTRAL metadata, so rendering a Console before a checkout is
|
|
2438
|
+
bound is a supported operation.
|
|
2439
|
+
"""
|
|
2440
|
+
return {
|
|
2441
|
+
"scope": "PLATFORM",
|
|
2442
|
+
"server": status(data_root),
|
|
2443
|
+
"central_database": central_database.details(data_root),
|
|
2444
|
+
"capacity_configuration": central_database.capacity_configuration(data_root),
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
|
|
2448
|
+
def _authenticated_consumer(connection: sqlite3.Connection, token: object, project_id: str) -> str | None:
|
|
2449
|
+
"""Authenticate an existing scoped CENTRAL consumer credential."""
|
|
2450
|
+
if not isinstance(token, str) or not token or len(token) > 4096:
|
|
2451
|
+
return None
|
|
2452
|
+
row = connection.execute("""SELECT c.consumer_id FROM ep_consumer_credentials c
|
|
2453
|
+
JOIN ep_consumer_registrations r ON r.consumer_id=c.consumer_id AND r.project_id=c.project_id
|
|
2454
|
+
WHERE c.verifier=? AND c.project_id=? AND c.revoked_at IS NULL
|
|
2455
|
+
AND (c.expires_at IS NULL OR c.expires_at>CURRENT_TIMESTAMP) AND r.status='ACTIVE'""", (verifier(token), project_id)).fetchone()
|
|
2456
|
+
return str(row[0]) if row else None
|
|
2457
|
+
|
|
2458
|
+
|
|
2459
|
+
def _admit_server_owned_file_inbox(
|
|
2460
|
+
data_root: Path, envelope: dict[str, object], receipt_id: str, received_at: str,
|
|
2461
|
+
) -> dict[str, object]:
|
|
2462
|
+
"""Use the canonical application service for the Server's File Inbox child.
|
|
2463
|
+
|
|
2464
|
+
The adapter bypasses only external consumer authentication. Request
|
|
2465
|
+
parsing, project/repository scope, execution-mode validation, idempotency,
|
|
2466
|
+
admission and lifecycle initialization remain owned by ``submission_service``.
|
|
2467
|
+
"""
|
|
2468
|
+
project_id = envelope.get("project_id")
|
|
2469
|
+
submission = envelope.get("submission")
|
|
2470
|
+
if not isinstance(project_id, str) or not isinstance(submission, Mapping):
|
|
2471
|
+
raise file_inbox.FileInboxError("MALFORMED_FILE")
|
|
2472
|
+
payload = dict(submission)
|
|
2473
|
+
payload["idempotency_key"] = receipt_id
|
|
2474
|
+
payload["transport_receipt_id"] = receipt_id
|
|
2475
|
+
payload["transport_received_at"] = received_at
|
|
2476
|
+
constraints = payload.get("constraints")
|
|
2477
|
+
if constraints is None:
|
|
2478
|
+
payload["constraints"] = {"transport_principal": "FILE_INBOX"}
|
|
2479
|
+
elif isinstance(constraints, Mapping):
|
|
2480
|
+
payload["constraints"] = {**constraints, "transport_principal": "FILE_INBOX"}
|
|
2481
|
+
try:
|
|
2482
|
+
with sqlite3.connect(data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
2483
|
+
request = submission_service.request_from_mapping(project_id, payload, transport="FILE_INBOX")
|
|
2484
|
+
return submission_service.submit(connection, request).to_dict()
|
|
2485
|
+
except submission_service.SubmissionError as error:
|
|
2486
|
+
raise file_inbox.FileInboxError(error.code) from error
|
|
2487
|
+
except (OSError, sqlite3.Error) as error:
|
|
2488
|
+
# A Server/database interruption is delivery availability, never an
|
|
2489
|
+
# execution failure. The physical item remains in ``processing`` for
|
|
2490
|
+
# the bounded File Inbox retry loop.
|
|
2491
|
+
raise URLError("CENTRAL_UNAVAILABLE") from error
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
class _HealthHandler(http.server.BaseHTTPRequestHandler):
|
|
2495
|
+
def _status(self) -> dict[str, object]:
|
|
2496
|
+
report = status(self.server.data_root) # type: ignore[attr-defined]
|
|
2497
|
+
worker = getattr(self.server, "lifecycle_worker", None)
|
|
2498
|
+
if worker is not None:
|
|
2499
|
+
report["lifecycle_worker"] = worker.diagnostics().to_dict()
|
|
2500
|
+
return report
|
|
2501
|
+
|
|
2502
|
+
def _send(self, status_code: int, payload: dict[str, object], instance_id: str | None = None) -> None:
|
|
2503
|
+
encoded = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
2504
|
+
self.send_response(status_code)
|
|
2505
|
+
self.send_header("Content-Type", "application/json")
|
|
2506
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
2507
|
+
if instance_id:
|
|
2508
|
+
self.send_header("EP-Server-Instance", instance_id)
|
|
2509
|
+
route = getattr(self, "_console_route", None)
|
|
2510
|
+
if route is not None:
|
|
2511
|
+
self.send_header("EP-Console-Route-Owner", route.owner)
|
|
2512
|
+
self.end_headers()
|
|
2513
|
+
self.wfile.write(encoded)
|
|
2514
|
+
|
|
2515
|
+
def _send_ndjson(self, entries: list[dict[str, object]]) -> None:
|
|
2516
|
+
encoded = ("\n".join(json.dumps(entry, sort_keys=True) for entry in entries) + ("\n" if entries else "")).encode("utf-8")
|
|
2517
|
+
self.send_response(200)
|
|
2518
|
+
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
|
|
2519
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
2520
|
+
self.send_header("Cache-Control", "no-store")
|
|
2521
|
+
route = getattr(self, "_console_route", None)
|
|
2522
|
+
if route is not None:
|
|
2523
|
+
self.send_header("EP-Console-Route-Owner", route.owner)
|
|
2524
|
+
self.end_headers()
|
|
2525
|
+
self.wfile.write(encoded)
|
|
2526
|
+
|
|
2527
|
+
def _send_console_asset(self, request: SplitResult) -> bool:
|
|
2528
|
+
"""Serve installed Console assets without selecting a project/root."""
|
|
2529
|
+
asset = _CONSOLE_STATIC_ASSETS.get(request.path)
|
|
2530
|
+
if asset is None:
|
|
2531
|
+
return False
|
|
2532
|
+
name, content_type = asset
|
|
2533
|
+
try:
|
|
2534
|
+
content = (console_presentation.ASSET_DIRECTORY / name).read_bytes()
|
|
2535
|
+
except OSError:
|
|
2536
|
+
self.send_error(404)
|
|
2537
|
+
return True
|
|
2538
|
+
self.send_response(200)
|
|
2539
|
+
self.send_header("Content-Type", content_type)
|
|
2540
|
+
self.send_header("Content-Length", str(len(content)))
|
|
2541
|
+
self.send_header("Cache-Control", "no-store")
|
|
2542
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
2543
|
+
self.end_headers()
|
|
2544
|
+
self.wfile.write(content)
|
|
2545
|
+
return True
|
|
2546
|
+
|
|
2547
|
+
def _no_project_platform_route(self, method: str, request: SplitResult) -> bool:
|
|
2548
|
+
"""Serve only explicit platform data when no project is selected.
|
|
2549
|
+
|
|
2550
|
+
Unsupported historical endpoints fail closed. In particular, this
|
|
2551
|
+
method never resolves a local repository binding merely to satisfy an
|
|
2552
|
+
old Dashboard helper.
|
|
2553
|
+
"""
|
|
2554
|
+
if method != "do_GET":
|
|
2555
|
+
return False
|
|
2556
|
+
if request.path == "/api/platform-status":
|
|
2557
|
+
self._send(200, _no_project_platform_projection(self.server.data_root)) # type: ignore[attr-defined]
|
|
2558
|
+
return True
|
|
2559
|
+
if request.path in {"/api/dashboard-snapshot", "/api/status"}:
|
|
2560
|
+
self._send(200, _no_project_console_snapshot(self.server.data_root)) # type: ignore[attr-defined]
|
|
2561
|
+
return True
|
|
2562
|
+
if request.path == "/api/events":
|
|
2563
|
+
self._stream_no_project_console_events()
|
|
2564
|
+
return True
|
|
2565
|
+
if request.path == "/health":
|
|
2566
|
+
report = status(self.server.data_root) # type: ignore[attr-defined]
|
|
2567
|
+
self._send(200 if report["healthy"] else 503, report, str(report["instance_id"]))
|
|
2568
|
+
return True
|
|
2569
|
+
if request.path == "/api/configuration":
|
|
2570
|
+
# CENTRAL-only settings presently supported by this phase. The
|
|
2571
|
+
# root-local dashboard configuration is intentionally unavailable.
|
|
2572
|
+
self._send(200, _central_console_configuration(self.server.data_root)) # type: ignore[attr-defined]
|
|
2573
|
+
return True
|
|
2574
|
+
if request.path == "/api/execution-runtime-status":
|
|
2575
|
+
self._send(200, _execution_runtime_status())
|
|
2576
|
+
return True
|
|
2577
|
+
if request.path == "/api/github-rate-limit":
|
|
2578
|
+
self._send(200, _github_rate_limit_status())
|
|
2579
|
+
return True
|
|
2580
|
+
if request.path == "/api/host-admin/diagnostics":
|
|
2581
|
+
self._send(200, host_admin.diagnostics(self.server.data_root)) # type: ignore[attr-defined]
|
|
2582
|
+
return True
|
|
2583
|
+
if request.path == "/api/provider-login-status":
|
|
2584
|
+
self._send(200, {"providers": _central_provider_readiness(self.server.data_root)}) # type: ignore[attr-defined]
|
|
2585
|
+
return True
|
|
2586
|
+
if request.path in {"/api/process-metrics", "/api/usage"}:
|
|
2587
|
+
self._send(200, {"scope": "PLATFORM", "available": False})
|
|
2588
|
+
return True
|
|
2589
|
+
if re.fullmatch(rf"/api/logs/(?:all|{PLATFORM_COMPONENT_ROUTE_PATTERN})", request.path):
|
|
2590
|
+
component = request.path.rsplit("/", 1)[-1]
|
|
2591
|
+
self._send(200, _central_console_component_logs(self.server.data_root, component) or {"error": "LOG_COMPONENT_UNKNOWN"}) # type: ignore[attr-defined]
|
|
2592
|
+
return True
|
|
2593
|
+
return False
|
|
2594
|
+
|
|
2595
|
+
def _send_central_database_backup(self) -> None:
|
|
2596
|
+
snapshot = central_database.snapshot(self.server.data_root) # type: ignore[attr-defined]
|
|
2597
|
+
if snapshot is None:
|
|
2598
|
+
self._send(503, {"error": "CENTRAL_DATABASE_UNAVAILABLE"})
|
|
2599
|
+
return
|
|
2600
|
+
filename = f"engineering-platform-central-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}.db"
|
|
2601
|
+
self.send_response(200)
|
|
2602
|
+
self.send_header("Content-Type", "application/vnd.sqlite3")
|
|
2603
|
+
self.send_header("Content-Disposition", _attachment_content_disposition(filename))
|
|
2604
|
+
self.send_header("Content-Length", str(len(snapshot)))
|
|
2605
|
+
self.send_header("Cache-Control", "no-store")
|
|
2606
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
2607
|
+
route = getattr(self, "_console_route", None)
|
|
2608
|
+
if route is not None:
|
|
2609
|
+
self.send_header("EP-Console-Route-Owner", route.owner)
|
|
2610
|
+
self.end_headers()
|
|
2611
|
+
self.wfile.write(snapshot)
|
|
2612
|
+
|
|
2613
|
+
def _send_central_data_export(self) -> None:
|
|
2614
|
+
"""Export one quiesced, portable snapshot of all durable CENTRAL data."""
|
|
2615
|
+
if _central_execution_active(self.server.data_root): # type: ignore[attr-defined]
|
|
2616
|
+
self._send(409, {"error": "CENTRAL_DATA_TRANSFER_BLOCKED"})
|
|
2617
|
+
return
|
|
2618
|
+
with self.server.central_data_transfer_lock: # type: ignore[attr-defined]
|
|
2619
|
+
self.server.central_data_transfer_active = True # type: ignore[attr-defined]
|
|
2620
|
+
try:
|
|
2621
|
+
self.server.dependabot_service.stop() # type: ignore[attr-defined]
|
|
2622
|
+
self.server.inbox_service.stop() # type: ignore[attr-defined]
|
|
2623
|
+
self.server.lifecycle_worker.stop() # type: ignore[attr-defined]
|
|
2624
|
+
if _central_execution_active(self.server.data_root): # type: ignore[attr-defined]
|
|
2625
|
+
raise central_data_transfer.CentralDataTransferError("CENTRAL_DATA_TRANSFER_BLOCKED")
|
|
2626
|
+
filename, snapshot = central_data_transfer.export_snapshot(self.server.data_root) # type: ignore[attr-defined]
|
|
2627
|
+
except (OSError, sqlite3.DatabaseError, central_data_transfer.CentralDataTransferError) as error:
|
|
2628
|
+
self._send(409, {"error": str(error)})
|
|
2629
|
+
return
|
|
2630
|
+
finally:
|
|
2631
|
+
self.server.lifecycle_worker.start() # type: ignore[attr-defined]
|
|
2632
|
+
self.server.inbox_service.start() # type: ignore[attr-defined]
|
|
2633
|
+
self.server.dependabot_service.start() # type: ignore[attr-defined]
|
|
2634
|
+
self.server.central_data_transfer_active = False # type: ignore[attr-defined]
|
|
2635
|
+
self.send_response(200)
|
|
2636
|
+
self.send_header("Content-Type", "application/zip")
|
|
2637
|
+
self.send_header("Content-Disposition", _attachment_content_disposition(filename))
|
|
2638
|
+
self.send_header("Content-Length", str(len(snapshot)))
|
|
2639
|
+
self.send_header("Cache-Control", "no-store")
|
|
2640
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
2641
|
+
self.end_headers()
|
|
2642
|
+
self.wfile.write(snapshot)
|
|
2643
|
+
|
|
2644
|
+
def _central_database_configuration(self, method: str) -> bool:
|
|
2645
|
+
request = urlsplit(self.path)
|
|
2646
|
+
if request.path == "/api/central-data/export" and method == "do_GET":
|
|
2647
|
+
self._send_central_data_export()
|
|
2648
|
+
return True
|
|
2649
|
+
if request.path == "/api/central-data/relocate/browse" and method == "do_POST":
|
|
2650
|
+
try:
|
|
2651
|
+
self._send(200, {"value": _choose_local_directory(self.server.data_root)}) # type: ignore[attr-defined]
|
|
2652
|
+
except ValueError as error:
|
|
2653
|
+
self._send(400, {"error": str(error)})
|
|
2654
|
+
return True
|
|
2655
|
+
if request.path == "/api/central-data/relocate" and method == "do_POST":
|
|
2656
|
+
try:
|
|
2657
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2658
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8")) if 0 < length <= 4096 else None
|
|
2659
|
+
if not isinstance(payload, dict) or set(payload) != {"directory"} or _central_execution_active(self.server.data_root): # type: ignore[attr-defined]
|
|
2660
|
+
raise ValueError("PLATFORM_DATA_RELOCATION_BLOCKED")
|
|
2661
|
+
result = installation_relocation.request(self.server.data_root, "PLATFORM_DATA", payload["directory"]) # type: ignore[attr-defined]
|
|
2662
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError, OSError) as error:
|
|
2663
|
+
self._send(409, {"error": str(error)})
|
|
2664
|
+
return True
|
|
2665
|
+
self._send(202, {**result, "restarting": True})
|
|
2666
|
+
Timer(0.5, lambda: os.kill(os.getpid(), signal.SIGTERM)).start()
|
|
2667
|
+
return True
|
|
2668
|
+
if request.path == "/api/central-data/import" and method == "do_POST":
|
|
2669
|
+
try:
|
|
2670
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2671
|
+
if self.headers.get("X-EP-Central-Import-Confirmed") != "true" or not 0 < length <= central_data_transfer.MAX_ARCHIVE_BYTES or _central_execution_active(self.server.data_root): # type: ignore[attr-defined]
|
|
2672
|
+
raise ValueError("CENTRAL_IMPORT_BLOCKED")
|
|
2673
|
+
imports = self.server.data_root / "runtime" / "central-data-imports" # type: ignore[attr-defined]
|
|
2674
|
+
imports.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
2675
|
+
upload = imports / f"upload-{uuid4().hex}.zip"
|
|
2676
|
+
with upload.open("wb") as output:
|
|
2677
|
+
remaining = length
|
|
2678
|
+
while remaining:
|
|
2679
|
+
chunk = self.rfile.read(min(1024 * 1024, remaining))
|
|
2680
|
+
if not chunk:
|
|
2681
|
+
raise ValueError("CENTRAL_IMPORT_UPLOAD_INCOMPLETE")
|
|
2682
|
+
output.write(chunk); remaining -= len(chunk)
|
|
2683
|
+
result = central_data_transfer.stage_import(self.server.data_root, upload) # type: ignore[attr-defined]
|
|
2684
|
+
upload.unlink(missing_ok=True)
|
|
2685
|
+
except (ValueError, OSError, central_data_transfer.CentralDataTransferError) as error:
|
|
2686
|
+
self._send(409, {"error": str(error)})
|
|
2687
|
+
return True
|
|
2688
|
+
self._send(202, {**result, "restarting": True})
|
|
2689
|
+
Timer(0.5, lambda: os.kill(os.getpid(), signal.SIGTERM)).start()
|
|
2690
|
+
return True
|
|
2691
|
+
if request.path == "/api/central-database/download" and method == "do_GET":
|
|
2692
|
+
self._send(410, {"error": "CENTRAL_DATABASE_DOWNLOAD_RETIRED"})
|
|
2693
|
+
return True
|
|
2694
|
+
if request.path != "/api/central-database/configuration":
|
|
2695
|
+
return False
|
|
2696
|
+
if method == "do_GET":
|
|
2697
|
+
self._send(200, central_database.maintenance_configuration(self.server.data_root)) # type: ignore[attr-defined]
|
|
2698
|
+
return True
|
|
2699
|
+
try:
|
|
2700
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2701
|
+
if not 0 < length <= 4096:
|
|
2702
|
+
raise ValueError
|
|
2703
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
2704
|
+
if not isinstance(payload, dict):
|
|
2705
|
+
raise ValueError
|
|
2706
|
+
result = central_database.update_maintenance_configuration(
|
|
2707
|
+
self.server.data_root, payload.get("interval_seconds"), # type: ignore[attr-defined]
|
|
2708
|
+
)
|
|
2709
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
2710
|
+
self._send(400, {"error": "CENTRAL_DATABASE_MAINTENANCE_INTERVAL_INVALID"})
|
|
2711
|
+
return True
|
|
2712
|
+
_audit_configuration_change(
|
|
2713
|
+
self.server.data_root, # type: ignore[attr-defined]
|
|
2714
|
+
scope="CENTRAL_DATABASE",
|
|
2715
|
+
key="maintenance_interval_seconds",
|
|
2716
|
+
previous=result.get("previous", "UNAVAILABLE"),
|
|
2717
|
+
value=result.get("interval_seconds", "UNAVAILABLE"),
|
|
2718
|
+
)
|
|
2719
|
+
self._send(200, result)
|
|
2720
|
+
return True
|
|
2721
|
+
|
|
2722
|
+
def _stream_console_events(self, root: Path, project_id: str) -> None:
|
|
2723
|
+
"""Stream the selected project from CENTRAL only.
|
|
2724
|
+
|
|
2725
|
+
``root`` is retained temporarily by the route's binding contract but
|
|
2726
|
+
is intentionally not read: a checkout cannot become state authority
|
|
2727
|
+
merely because it is attached to a selected project.
|
|
2728
|
+
"""
|
|
2729
|
+
self.send_response(200)
|
|
2730
|
+
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
2731
|
+
self.send_header("Cache-Control", "no-store")
|
|
2732
|
+
self.end_headers()
|
|
2733
|
+
try:
|
|
2734
|
+
# Event delivery is a platform concern even when its snapshot has
|
|
2735
|
+
# a selected-project projection. It must therefore use the same
|
|
2736
|
+
# CENTRAL-owned interval as the no-project Console, rather than a
|
|
2737
|
+
# repository/root-local Dashboard preference.
|
|
2738
|
+
stream_interval = int(
|
|
2739
|
+
central_database.console_interval_configuration(self.server.data_root)[ # type: ignore[attr-defined]
|
|
2740
|
+
"dashboard_stream_interval_seconds"
|
|
2741
|
+
]
|
|
2742
|
+
)
|
|
2743
|
+
self.wfile.write(f"retry: {stream_interval * 1000}\n\n".encode())
|
|
2744
|
+
previous: bytes | None = None
|
|
2745
|
+
for iteration in range(300):
|
|
2746
|
+
snapshot = json.dumps(
|
|
2747
|
+
_central_console_project_snapshot(self.server.data_root, project_id),
|
|
2748
|
+
separators=(",", ":"),
|
|
2749
|
+
).encode("utf-8") # type: ignore[attr-defined]
|
|
2750
|
+
if snapshot != previous:
|
|
2751
|
+
self.wfile.write(b"event: dashboard\ndata: " + snapshot + b"\n\n")
|
|
2752
|
+
self.wfile.flush()
|
|
2753
|
+
previous = snapshot
|
|
2754
|
+
elif iteration and iteration % 15 == 0:
|
|
2755
|
+
self.wfile.write(b": keepalive\n\n")
|
|
2756
|
+
self.wfile.flush()
|
|
2757
|
+
interval = int(
|
|
2758
|
+
central_database.console_interval_configuration(self.server.data_root)[ # type: ignore[attr-defined]
|
|
2759
|
+
"dashboard_stream_interval_seconds"
|
|
2760
|
+
]
|
|
2761
|
+
)
|
|
2762
|
+
if interval != stream_interval:
|
|
2763
|
+
self.wfile.write(f"retry: {interval * 1000}\n\n".encode())
|
|
2764
|
+
self.wfile.flush()
|
|
2765
|
+
stream_interval = interval
|
|
2766
|
+
time.sleep(stream_interval)
|
|
2767
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2768
|
+
return
|
|
2769
|
+
|
|
2770
|
+
def _stream_no_project_console_events(self) -> None:
|
|
2771
|
+
"""Send a CENTRAL-only event that completes the shared Console shell."""
|
|
2772
|
+
self.send_response(200)
|
|
2773
|
+
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
2774
|
+
self.send_header("Cache-Control", "no-store")
|
|
2775
|
+
self.send_header("EP-Console-Route-Owner", "PLATFORM")
|
|
2776
|
+
self.end_headers()
|
|
2777
|
+
try:
|
|
2778
|
+
stream_interval = int(
|
|
2779
|
+
central_database.console_interval_configuration(self.server.data_root)[ # type: ignore[attr-defined]
|
|
2780
|
+
"dashboard_stream_interval_seconds"
|
|
2781
|
+
]
|
|
2782
|
+
)
|
|
2783
|
+
self.wfile.write(f"retry: {stream_interval * 1000}\n\n".encode())
|
|
2784
|
+
previous: bytes | None = None
|
|
2785
|
+
for iteration in range(300):
|
|
2786
|
+
payload = json.dumps(
|
|
2787
|
+
_no_project_console_snapshot(self.server.data_root), separators=(",", ":") # type: ignore[attr-defined]
|
|
2788
|
+
).encode("utf-8")
|
|
2789
|
+
if payload != previous:
|
|
2790
|
+
self.wfile.write(b"event: dashboard\ndata: " + payload + b"\n\n")
|
|
2791
|
+
self.wfile.flush()
|
|
2792
|
+
previous = payload
|
|
2793
|
+
elif iteration and iteration % 15 == 0:
|
|
2794
|
+
self.wfile.write(b": keepalive\n\n")
|
|
2795
|
+
self.wfile.flush()
|
|
2796
|
+
time.sleep(stream_interval)
|
|
2797
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2798
|
+
return
|
|
2799
|
+
|
|
2800
|
+
def _stream_project_console_events(self, project_id: str) -> None:
|
|
2801
|
+
"""Keep the selected project's CENTRAL-only dashboard stream alive."""
|
|
2802
|
+
self.send_response(200)
|
|
2803
|
+
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
2804
|
+
self.send_header("Cache-Control", "no-store")
|
|
2805
|
+
self.send_header("EP-Console-Route-Owner", "PLATFORM")
|
|
2806
|
+
self.end_headers()
|
|
2807
|
+
try:
|
|
2808
|
+
stream_interval = int(
|
|
2809
|
+
central_database.console_interval_configuration(self.server.data_root)[ # type: ignore[attr-defined]
|
|
2810
|
+
"dashboard_stream_interval_seconds"
|
|
2811
|
+
]
|
|
2812
|
+
)
|
|
2813
|
+
self.wfile.write(f"retry: {stream_interval * 1000}\n\n".encode())
|
|
2814
|
+
previous: bytes | None = None
|
|
2815
|
+
for iteration in range(300):
|
|
2816
|
+
payload = json.dumps(
|
|
2817
|
+
_central_console_project_snapshot(self.server.data_root, project_id), # type: ignore[attr-defined]
|
|
2818
|
+
separators=(",", ":"),
|
|
2819
|
+
).encode("utf-8")
|
|
2820
|
+
if payload != previous:
|
|
2821
|
+
self.wfile.write(b"event: dashboard\ndata: " + payload + b"\n\n")
|
|
2822
|
+
self.wfile.flush()
|
|
2823
|
+
previous = payload
|
|
2824
|
+
elif iteration and iteration % 15 == 0:
|
|
2825
|
+
self.wfile.write(b": keepalive\n\n")
|
|
2826
|
+
self.wfile.flush()
|
|
2827
|
+
time.sleep(stream_interval)
|
|
2828
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2829
|
+
return
|
|
2830
|
+
|
|
2831
|
+
def _delegate_dashboard(self, method: str) -> None:
|
|
2832
|
+
"""Route the transitional Console after CENTRAL validates its scope."""
|
|
2833
|
+
request = urlsplit(self.path)
|
|
2834
|
+
# Resolve ownership once, before any project identity is read. The
|
|
2835
|
+
# header makes the runtime contract observable to browser/integration
|
|
2836
|
+
# coverage without granting a selected project any authority.
|
|
2837
|
+
self._console_route = console_route_ownership.route_owner(method.removeprefix("do_"), request.path)
|
|
2838
|
+
# The export boundary stops all installed writer services. Refuse a
|
|
2839
|
+
# concurrent Console mutation as well, rather than claiming a ZIP is
|
|
2840
|
+
# a point-in-time snapshot while an HTTP request can still alter it.
|
|
2841
|
+
if method == "do_POST" and getattr(self.server, "central_data_transfer_active", False):
|
|
2842
|
+
self._send(423, {"error": "CENTRAL_DATA_TRANSFER_IN_PROGRESS"})
|
|
2843
|
+
return
|
|
2844
|
+
if method == "do_GET" and self._send_console_asset(request):
|
|
2845
|
+
return
|
|
2846
|
+
if request.path.startswith("/api/configuration/file-inbox/relocate"):
|
|
2847
|
+
self._send(410, {"error": "FILE_INBOX_PARTIAL_RELOCATION_RETIRED"})
|
|
2848
|
+
return
|
|
2849
|
+
if method == "do_POST" and re.fullmatch(r"/api/configuration/inbox-location(?:/browse)?", request.path):
|
|
2850
|
+
self._send(410, {"error": "INBOX_WATCHER_CONFIGURATION_RETIRED"})
|
|
2851
|
+
return
|
|
2852
|
+
if re.fullmatch(r"/api/logs/(?:inbox|dashboard)", request.path):
|
|
2853
|
+
# These former dashboard-owned streams are deliberately absent
|
|
2854
|
+
# from the canonical CENTRAL component model. Handle them before
|
|
2855
|
+
# scope resolution so a stale caller cannot turn a retired route
|
|
2856
|
+
# into a project-delegation failure.
|
|
2857
|
+
self._send(410, {"error": "LEGACY_COMPONENT_LOG_ROUTE_RETIRED"})
|
|
2858
|
+
return
|
|
2859
|
+
if re.fullmatch(
|
|
2860
|
+
rf"/api/components/{RETIRED_COMPONENT_ALIAS_ROUTE_PATTERN}/(?:details|restart)",
|
|
2861
|
+
request.path,
|
|
2862
|
+
):
|
|
2863
|
+
# A retired name is never normalized to a supported component.
|
|
2864
|
+
# Reject it before project resolution so it cannot acquire a
|
|
2865
|
+
# project, lifecycle or repair interpretation as a side effect.
|
|
2866
|
+
self._send(410, {"error": "LEGACY_COMPONENT_AUTHORITY_RETIRED"})
|
|
2867
|
+
return
|
|
2868
|
+
log_match = re.fullmatch(rf"/api/logs/(all|{PLATFORM_COMPONENT_ROUTE_PATTERN})", request.path)
|
|
2869
|
+
if log_match and method == "do_GET":
|
|
2870
|
+
query = parse_qs(request.query)
|
|
2871
|
+
try:
|
|
2872
|
+
payload = _central_console_component_logs(
|
|
2873
|
+
self.server.data_root, log_match.group(1), query,
|
|
2874
|
+
export_all=(query.get("format") or [""])[0] == "ndjson",
|
|
2875
|
+
) # type: ignore[attr-defined]
|
|
2876
|
+
except ValueError:
|
|
2877
|
+
self._send(400, {"error": "LOG_QUERY_INVALID"})
|
|
2878
|
+
return
|
|
2879
|
+
if payload is None:
|
|
2880
|
+
self._send(404, {"error": "LOG_COMPONENT_UNKNOWN"})
|
|
2881
|
+
elif (query.get("format") or [""])[0] == "ndjson":
|
|
2882
|
+
self._send_ndjson(list(payload["entries"]))
|
|
2883
|
+
else:
|
|
2884
|
+
self._send(200, payload)
|
|
2885
|
+
return
|
|
2886
|
+
if log_match and method == "do_POST":
|
|
2887
|
+
try:
|
|
2888
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
2889
|
+
raise ValueError
|
|
2890
|
+
payload = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))).decode("utf-8"))
|
|
2891
|
+
if not isinstance(payload, dict) or set(payload) != {"component"} or not isinstance(payload["component"], str):
|
|
2892
|
+
raise ValueError
|
|
2893
|
+
result = _clear_central_console_component_logs(self.server.data_root, payload["component"]) # type: ignore[attr-defined]
|
|
2894
|
+
if result is None:
|
|
2895
|
+
raise ValueError
|
|
2896
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
2897
|
+
self._send(400, {"error": "LOG_COMPONENT_INVALID"})
|
|
2898
|
+
else:
|
|
2899
|
+
self._send(200, result)
|
|
2900
|
+
return
|
|
2901
|
+
# Platform health is deliberately independent of the browser's
|
|
2902
|
+
# selected project preference, so the same components remain visible
|
|
2903
|
+
# in both Console modes.
|
|
2904
|
+
if method == "do_GET" and request.path == "/health":
|
|
2905
|
+
report = status(self.server.data_root) # type: ignore[attr-defined]
|
|
2906
|
+
self._send(200 if report["healthy"] else 503, report, str(report["instance_id"]))
|
|
2907
|
+
return
|
|
2908
|
+
if method == "do_GET" and request.path == "/api/host-admin/diagnostics":
|
|
2909
|
+
# Host Admin has an installation-only root and is intentionally
|
|
2910
|
+
# resolved before any selected-project header is inspected.
|
|
2911
|
+
self._send(200, host_admin.diagnostics(self.server.data_root)) # type: ignore[attr-defined]
|
|
2912
|
+
return
|
|
2913
|
+
component_match = re.fullmatch(r"/api/components/([a-z_]+)/details", request.path)
|
|
2914
|
+
if component_match and component_match.group(1) not in PLATFORM_COMPONENT_IDS:
|
|
2915
|
+
component_match = None
|
|
2916
|
+
if method == "do_GET" and component_match:
|
|
2917
|
+
detail = _platform_component_detail(self.server.data_root, component_match.group(1)) # type: ignore[attr-defined]
|
|
2918
|
+
self._send(200, detail) if detail is not None else self._send(404, {"error": "COMPONENT_UNKNOWN"})
|
|
2919
|
+
return
|
|
2920
|
+
restart_match = re.fullmatch(r"/api/components/([a-z_]+)/restart", request.path)
|
|
2921
|
+
if method == "do_POST" and restart_match:
|
|
2922
|
+
try:
|
|
2923
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
2924
|
+
raise ValueError
|
|
2925
|
+
if self.rfile.read(int(self.headers.get("Content-Length", "0"))) != b"{}":
|
|
2926
|
+
raise ValueError
|
|
2927
|
+
result = _restart_platform_component(self.server.data_root, restart_match.group(1)) # type: ignore[attr-defined]
|
|
2928
|
+
except (ValueError, OSError):
|
|
2929
|
+
self._send(409, {"error": "COMPONENT_RESTART_UNAVAILABLE"})
|
|
2930
|
+
else:
|
|
2931
|
+
self._send(202, result)
|
|
2932
|
+
return
|
|
2933
|
+
if self._central_database_configuration(method):
|
|
2934
|
+
return
|
|
2935
|
+
if request.path == "/api/provider-capacity":
|
|
2936
|
+
if method != "do_GET":
|
|
2937
|
+
self._send(405, {"error": "METHOD_NOT_ALLOWED"})
|
|
2938
|
+
else:
|
|
2939
|
+
self._send(200, _provider_capacity_projection(self.server.data_root)) # type: ignore[attr-defined]
|
|
2940
|
+
return
|
|
2941
|
+
if request.path == "/api/provider-login-status" and method == "do_GET":
|
|
2942
|
+
self._send(200, {"providers": _central_provider_readiness(self.server.data_root)}) # type: ignore[attr-defined]
|
|
2943
|
+
return
|
|
2944
|
+
if request.path == "/api/execution-runtime-status" and method == "do_GET":
|
|
2945
|
+
# Validation is an installation capability, independent of the
|
|
2946
|
+
# selected project. Keep it out of the legacy checkout delegate.
|
|
2947
|
+
self._send(200, _execution_runtime_status())
|
|
2948
|
+
return
|
|
2949
|
+
if request.path == "/api/execution-runtime/repair" and method == "do_POST":
|
|
2950
|
+
try:
|
|
2951
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
2952
|
+
raise ValueError
|
|
2953
|
+
if self.rfile.read(int(self.headers.get("Content-Length", "0"))) != b"{}":
|
|
2954
|
+
raise ValueError
|
|
2955
|
+
runtime = _execution_runtime_status()
|
|
2956
|
+
if runtime["state"] != "READY":
|
|
2957
|
+
raise ValueError
|
|
2958
|
+
except (ValueError, OSError):
|
|
2959
|
+
self._send(409, {"error": "EXECUTION_RUNTIME_UNAVAILABLE"})
|
|
2960
|
+
return
|
|
2961
|
+
self._send(200, {"rechecked": True, "runtime": runtime, "scope": "PLATFORM"})
|
|
2962
|
+
return
|
|
2963
|
+
if request.path == "/api/provider-login/repair" and method == "do_POST":
|
|
2964
|
+
# Provider installation and interactive sign-in are host-wide
|
|
2965
|
+
# operations. They must never fall through to the historical
|
|
2966
|
+
# checkout-bound dashboard handler: on the <geen> projection that
|
|
2967
|
+
# handler rejects the request for lack of a selected project and
|
|
2968
|
+
# the subsequent readiness refresh misleadingly becomes a check
|
|
2969
|
+
# failure.
|
|
2970
|
+
try:
|
|
2971
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
2972
|
+
raise ValueError
|
|
2973
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2974
|
+
if not 0 < length <= 1024:
|
|
2975
|
+
raise ValueError
|
|
2976
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
2977
|
+
_central_provider_repair(self.server.data_root, payload) # type: ignore[attr-defined]
|
|
2978
|
+
except (OSError, RuntimeError, ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
2979
|
+
self._send(409, {"error": "PROVIDER_REPAIR_UNAVAILABLE"})
|
|
2980
|
+
return
|
|
2981
|
+
self._send(202, {"started": True, "scope": "PLATFORM"})
|
|
2982
|
+
return
|
|
2983
|
+
if request.path == "/api/provider-login/logout" and method == "do_POST":
|
|
2984
|
+
# Logout is the companion host-wide action to login. Do not let
|
|
2985
|
+
# the installed no-project Console fall through to the retired
|
|
2986
|
+
# checkout handler, which rejects it before the CLI can run.
|
|
2987
|
+
try:
|
|
2988
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
2989
|
+
raise ValueError
|
|
2990
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2991
|
+
if not 0 < length <= 1024:
|
|
2992
|
+
raise ValueError
|
|
2993
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
2994
|
+
_central_provider_logout(self.server.data_root, payload) # type: ignore[attr-defined]
|
|
2995
|
+
except (OSError, RuntimeError, ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
2996
|
+
self._send(409, {"error": "PROVIDER_LOGOUT_UNAVAILABLE"})
|
|
2997
|
+
return
|
|
2998
|
+
self._send(200, {"logged_out": True, "scope": "PLATFORM"})
|
|
2999
|
+
return
|
|
3000
|
+
if request.path == "/api/provider-capacity/configuration":
|
|
3001
|
+
if method == "do_GET":
|
|
3002
|
+
self._send(200, central_database.capacity_configuration(self.server.data_root)) # type: ignore[attr-defined]
|
|
3003
|
+
return
|
|
3004
|
+
try:
|
|
3005
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
3006
|
+
raise ValueError
|
|
3007
|
+
payload = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))).decode("utf-8"))
|
|
3008
|
+
reserve = payload.get("codex_capacity_reserve_percent") if isinstance(payload, dict) else None
|
|
3009
|
+
live = _provider_capacity_projection(self.server.data_root) # type: ignore[attr-defined]
|
|
3010
|
+
remaining = _remaining_rate_limit_capacity(live["rate_limits"])
|
|
3011
|
+
if not isinstance(reserve, int) or isinstance(reserve, bool) or (reserve and (remaining is None or reserve > remaining)):
|
|
3012
|
+
raise ValueError
|
|
3013
|
+
result = central_database.update_capacity_configuration(self.server.data_root, reserve) # type: ignore[attr-defined]
|
|
3014
|
+
_audit_configuration_change(
|
|
3015
|
+
self.server.data_root, # type: ignore[attr-defined]
|
|
3016
|
+
scope="PROVIDER_CAPACITY",
|
|
3017
|
+
key="codex_capacity_reserve_percent",
|
|
3018
|
+
previous=result.get("previous", "UNAVAILABLE"),
|
|
3019
|
+
value=result.get("codex_capacity_reserve_percent", "UNAVAILABLE"),
|
|
3020
|
+
)
|
|
3021
|
+
self._send(200, result)
|
|
3022
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
3023
|
+
self._send(409, {"error": "CODEX_CAPACITY_RESERVE_INVALID"})
|
|
3024
|
+
return
|
|
3025
|
+
if request.path == "/api/configuration" and method == "do_POST":
|
|
3026
|
+
try:
|
|
3027
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
3028
|
+
raise ValueError
|
|
3029
|
+
payload = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))).decode("utf-8"))
|
|
3030
|
+
if not isinstance(payload, dict) or set(payload) != {"key", "value", "previous"}:
|
|
3031
|
+
raise ValueError
|
|
3032
|
+
result = central_database.update_console_interval_configuration(
|
|
3033
|
+
self.server.data_root, payload["key"], payload["value"],
|
|
3034
|
+
) # type: ignore[attr-defined]
|
|
3035
|
+
_audit_configuration_change(
|
|
3036
|
+
self.server.data_root, # type: ignore[attr-defined]
|
|
3037
|
+
scope="OPERATIONS_CONSOLE",
|
|
3038
|
+
key=str(result["key"]),
|
|
3039
|
+
previous=result.get("previous", "UNAVAILABLE"),
|
|
3040
|
+
value=result.get("value", "UNAVAILABLE"),
|
|
3041
|
+
)
|
|
3042
|
+
self._send(200, result)
|
|
3043
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
3044
|
+
self._send(409, {"error": "CONSOLE_CONFIGURATION_INVALID"})
|
|
3045
|
+
return
|
|
3046
|
+
if method == "do_POST" and request.path == "/api/runtime-directory/open":
|
|
3047
|
+
# This action historically resolved the first bound checkout.
|
|
3048
|
+
# The installed CENTRAL Console deliberately has no root-bound
|
|
3049
|
+
# runtime action; runtime paths remain display-only diagnostics.
|
|
3050
|
+
self._send(410, {"error": "RUNTIME_DIRECTORY_RETIRED"})
|
|
3051
|
+
return
|
|
3052
|
+
selected = self.headers.get("X-Engineering-Platform-Project")
|
|
3053
|
+
if not selected:
|
|
3054
|
+
selected = (parse_qs(request.query).get("project") or [None])[0]
|
|
3055
|
+
# Listing projects is a CENTRAL-only operation. Do not validate or
|
|
3056
|
+
# inspect any checkout until a selected project needs a transitional
|
|
3057
|
+
# project route below.
|
|
3058
|
+
projects = _console_projects(self.server.data_root) # type: ignore[attr-defined]
|
|
3059
|
+
project_ids = {item["project_id"] for item in projects}
|
|
3060
|
+
if method == "do_GET" and isinstance(selected, str) and selected in project_ids:
|
|
3061
|
+
# Slice B: the core project read model is available even when its
|
|
3062
|
+
# checkout has been deleted or rebound. Do this before the
|
|
3063
|
+
# transitional handler can resolve a root.
|
|
3064
|
+
if request.path == "/":
|
|
3065
|
+
document = _selected_project_console_document(selected, projects, self.server.data_root) # type: ignore[attr-defined]
|
|
3066
|
+
self.send_response(200)
|
|
3067
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
3068
|
+
self.send_header("Content-Length", str(len(document)))
|
|
3069
|
+
self.send_header("Cache-Control", "no-store")
|
|
3070
|
+
self.end_headers()
|
|
3071
|
+
self.wfile.write(document)
|
|
3072
|
+
return
|
|
3073
|
+
if request.path == "/api/configuration":
|
|
3074
|
+
self._send(200, _central_console_configuration(self.server.data_root)) # type: ignore[attr-defined]
|
|
3075
|
+
return
|
|
3076
|
+
if re.fullmatch(rf"/api/logs/(?:all|{PLATFORM_COMPONENT_ROUTE_PATTERN})", request.path):
|
|
3077
|
+
component = request.path.rsplit("/", 1)[-1]
|
|
3078
|
+
payload = _central_console_component_logs(self.server.data_root, component) # type: ignore[attr-defined]
|
|
3079
|
+
if payload is None:
|
|
3080
|
+
self._send(404, {"error": "LOG_COMPONENT_UNKNOWN"})
|
|
3081
|
+
else:
|
|
3082
|
+
self._send(200, payload)
|
|
3083
|
+
return
|
|
3084
|
+
if request.path in {"/api/dashboard-snapshot", "/api/status"}:
|
|
3085
|
+
self._send(200, _central_console_project_snapshot(self.server.data_root, selected)) # type: ignore[attr-defined]
|
|
3086
|
+
return
|
|
3087
|
+
if request.path == "/api/prompt-history":
|
|
3088
|
+
snapshot = _central_console_project_snapshot(self.server.data_root, selected) # type: ignore[attr-defined]
|
|
3089
|
+
# Keep the established Console list contract while changing
|
|
3090
|
+
# only its authority source.
|
|
3091
|
+
encoded = json.dumps(snapshot["runs"], separators=(",", ":")).encode("utf-8")
|
|
3092
|
+
self.send_response(200)
|
|
3093
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
3094
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
3095
|
+
self.send_header("Cache-Control", "no-store")
|
|
3096
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
3097
|
+
self.end_headers()
|
|
3098
|
+
self.wfile.write(encoded)
|
|
3099
|
+
return
|
|
3100
|
+
report_match = re.fullmatch(r"/api/prompt-history/([a-z0-9][a-z0-9-]{0,63})/report", request.path)
|
|
3101
|
+
if report_match:
|
|
3102
|
+
content = _central_console_report(self.server.data_root, selected, report_match.group(1)) # type: ignore[attr-defined]
|
|
3103
|
+
if content is None:
|
|
3104
|
+
self._send(404, {"error": "REPORT_NOT_FOUND"})
|
|
3105
|
+
return
|
|
3106
|
+
self.send_response(200)
|
|
3107
|
+
self.send_header("Content-Type", "text/markdown; charset=utf-8")
|
|
3108
|
+
self.send_header(
|
|
3109
|
+
"Content-Disposition",
|
|
3110
|
+
_report_content_disposition(report_match.group(1)),
|
|
3111
|
+
)
|
|
3112
|
+
self.send_header("Content-Length", str(len(content)))
|
|
3113
|
+
self.send_header("Cache-Control", "no-store")
|
|
3114
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
3115
|
+
self.end_headers()
|
|
3116
|
+
self.wfile.write(content)
|
|
3117
|
+
return
|
|
3118
|
+
chat_match = re.fullmatch(r"/api/prompt-history/([a-z0-9][a-z0-9-]{0,63})/chat", request.path)
|
|
3119
|
+
if chat_match:
|
|
3120
|
+
messages = _central_console_chat_history(self.server.data_root, selected, chat_match.group(1)) # type: ignore[attr-defined]
|
|
3121
|
+
if messages is None:
|
|
3122
|
+
self._send(404, {"error": "RUN_NOT_FOUND"})
|
|
3123
|
+
else:
|
|
3124
|
+
self._send(200, {"messages": messages, "source": "CENTRAL"})
|
|
3125
|
+
return
|
|
3126
|
+
detail_match = re.fullmatch(r"/api/prompt-history/([a-z0-9][a-z0-9-]{0,63})/details", request.path)
|
|
3127
|
+
if detail_match:
|
|
3128
|
+
detail = _central_console_run_detail(self.server.data_root, selected, detail_match.group(1)) # type: ignore[attr-defined]
|
|
3129
|
+
if detail is None:
|
|
3130
|
+
self._send(404, {"error": "RUN_NOT_FOUND"})
|
|
3131
|
+
else:
|
|
3132
|
+
self._send(200, {"project_id": selected, "run": detail, "source": "CENTRAL"})
|
|
3133
|
+
return
|
|
3134
|
+
telemetry_match = re.fullmatch(r"/api/telemetry/([0-9]{4}-[0-9]{2}-[0-9]{2})", request.path)
|
|
3135
|
+
if telemetry_match:
|
|
3136
|
+
detail = _central_console_telemetry_detail(
|
|
3137
|
+
self.server.data_root, selected, telemetry_match.group(1), # type: ignore[attr-defined]
|
|
3138
|
+
)
|
|
3139
|
+
if detail is None:
|
|
3140
|
+
self._send(404, {"error": "TELEMETRY_NOT_FOUND"})
|
|
3141
|
+
else:
|
|
3142
|
+
self._send(200, detail)
|
|
3143
|
+
return
|
|
3144
|
+
if request.path == "/api/events":
|
|
3145
|
+
self._stream_project_console_events(selected)
|
|
3146
|
+
return
|
|
3147
|
+
if method == "do_POST" and isinstance(selected, str) and selected in project_ids and (
|
|
3148
|
+
request.path == "/api/configuration" or request.path.startswith("/api/logs/")
|
|
3149
|
+
):
|
|
3150
|
+
# The local dashboard's mutable metadata/log controls have no
|
|
3151
|
+
# CENTRAL contract yet. Fail closed rather than mutating a
|
|
3152
|
+
# checkout-local store for compatibility.
|
|
3153
|
+
self._send(405, {"error": "CONSOLE_CONFIGURATION_MUTATION_UNAVAILABLE"})
|
|
3154
|
+
return
|
|
3155
|
+
if method == "do_POST" and request.path in {"/api/execution-dismiss", "/api/execution-retry"}:
|
|
3156
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
3157
|
+
self._send(403, {"error": "INVALID_ORIGIN"})
|
|
3158
|
+
return
|
|
3159
|
+
if not isinstance(selected, str) or selected not in project_ids:
|
|
3160
|
+
self._send(409, {"error": "CONSOLE_PROJECT_UNAVAILABLE"})
|
|
3161
|
+
return
|
|
3162
|
+
# The preserved execution lifecycle is loaded only when its
|
|
3163
|
+
# project-scoped mutation is requested. Importing the canonical
|
|
3164
|
+
# Server must not load retired watcher-era implementation modules.
|
|
3165
|
+
from .parity_lifecycle_dispatcher import (
|
|
3166
|
+
ParityLifecycleDispatchError,
|
|
3167
|
+
dismiss_operator_gate,
|
|
3168
|
+
retry_operator_gate,
|
|
3169
|
+
)
|
|
3170
|
+
try:
|
|
3171
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
3172
|
+
if not 2 <= length <= 256:
|
|
3173
|
+
raise ValueError
|
|
3174
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
3175
|
+
run_id = payload.get("run_id") if isinstance(payload, dict) and set(payload) == {"run_id"} else None
|
|
3176
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
3177
|
+
raise ValueError
|
|
3178
|
+
if request.path == "/api/execution-dismiss":
|
|
3179
|
+
result: dict[str, object] = dismiss_operator_gate(
|
|
3180
|
+
self.server.data_root, project_id=selected, run_id=run_id, # type: ignore[attr-defined]
|
|
3181
|
+
)
|
|
3182
|
+
else:
|
|
3183
|
+
result = retry_operator_gate(
|
|
3184
|
+
self.server.data_root, project_id=selected, run_id=run_id, # type: ignore[attr-defined]
|
|
3185
|
+
).to_dict()
|
|
3186
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
3187
|
+
self._send(400, {"error": "INVALID_REQUEST"})
|
|
3188
|
+
return
|
|
3189
|
+
except ParityLifecycleDispatchError as error:
|
|
3190
|
+
self._send(409, {"error": str(error)})
|
|
3191
|
+
return
|
|
3192
|
+
self._send(200, result)
|
|
3193
|
+
return
|
|
3194
|
+
if isinstance(selected, str) and selected in project_ids:
|
|
3195
|
+
# No supported CENTRAL Console route may fall through to the
|
|
3196
|
+
# retained dashboard handler. New routes must be added above
|
|
3197
|
+
# with an explicit Server/CENTRAL authority classification.
|
|
3198
|
+
self._send(404 if method == "do_GET" else 405, {"error": "CENTRAL_CONSOLE_ROUTE_UNAVAILABLE"})
|
|
3199
|
+
return
|
|
3200
|
+
if method == "do_POST" and request.path == "/api/dashboard-translate":
|
|
3201
|
+
if self.headers.get("Origin") not in {None, "", f"http://{self.headers.get('Host', '')}"}:
|
|
3202
|
+
self._send(403, {"error": "INVALID_ORIGIN"})
|
|
3203
|
+
return
|
|
3204
|
+
if not isinstance(selected, str) or selected not in project_ids:
|
|
3205
|
+
self._send(409, {"error": "CONSOLE_PROJECT_UNAVAILABLE"})
|
|
3206
|
+
return
|
|
3207
|
+
try:
|
|
3208
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
3209
|
+
if not 2 <= length <= 4096:
|
|
3210
|
+
raise ValueError
|
|
3211
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
3212
|
+
if not isinstance(payload, dict) or set(payload) != {"locale", "texts"}:
|
|
3213
|
+
raise ValueError
|
|
3214
|
+
translations = dashboard_translation.translate(payload["locale"], payload["texts"])
|
|
3215
|
+
except dashboard_translation.DashboardTranslationError as error:
|
|
3216
|
+
status_code = 400 if str(error).endswith(("LOCALE_INVALID", "REQUEST_INVALID")) else 503
|
|
3217
|
+
self._send(status_code, {"error": str(error)})
|
|
3218
|
+
return
|
|
3219
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
3220
|
+
self._send(400, {"error": "DASHBOARD_TRANSLATION_REQUEST_INVALID"})
|
|
3221
|
+
return
|
|
3222
|
+
self._send(200, {"translations": translations})
|
|
3223
|
+
return
|
|
3224
|
+
if method == "do_GET" and request.path == "/" and selected in {None, ""}:
|
|
3225
|
+
# No selection is a valid view. It renders only the host-wide
|
|
3226
|
+
# controls and never substitutes the first project for content.
|
|
3227
|
+
document = _no_project_console_document(projects, self.server.data_root) # type: ignore[attr-defined]
|
|
3228
|
+
self.send_response(200)
|
|
3229
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
3230
|
+
self.send_header("Content-Length", str(len(document)))
|
|
3231
|
+
self.send_header("Cache-Control", "no-store")
|
|
3232
|
+
self.end_headers()
|
|
3233
|
+
self.wfile.write(document)
|
|
3234
|
+
return
|
|
3235
|
+
if selected in {None, ""}:
|
|
3236
|
+
if self._no_project_platform_route(method, request):
|
|
3237
|
+
return
|
|
3238
|
+
self._send(409, {"error": "CONSOLE_PROJECT_UNAVAILABLE"})
|
|
3239
|
+
return
|
|
3240
|
+
if not isinstance(selected, str) or selected not in project_ids:
|
|
3241
|
+
self._send(409, {"error": "CONSOLE_PROJECT_UNAVAILABLE"})
|
|
3242
|
+
return
|
|
3243
|
+
# Reaching this point would mean a route escaped the explicit Console
|
|
3244
|
+
# projection table above. Never restore the historical root delegate.
|
|
3245
|
+
self._send(404 if method == "do_GET" else 405, {"error": "CENTRAL_CONSOLE_ROUTE_UNAVAILABLE"})
|
|
3246
|
+
|
|
3247
|
+
def do_GET(self) -> None: # noqa: N802
|
|
3248
|
+
request = urlsplit(self.path)
|
|
3249
|
+
if request.path in {HTTP_JSON_OPENAPI_PATH, "/swagger.json"}:
|
|
3250
|
+
self._send(200, _http_json_openapi_document())
|
|
3251
|
+
return
|
|
3252
|
+
if request.path == "/diagnostics/topology":
|
|
3253
|
+
try:
|
|
3254
|
+
self._send(200, operations_projection(self.server.data_root), initialize(self.server.data_root).instance_id) # type: ignore[attr-defined]
|
|
3255
|
+
except ServerConfigurationError:
|
|
3256
|
+
self._send(503, {"error": "TOPOLOGY_DIAGNOSTIC_UNAVAILABLE"})
|
|
3257
|
+
return
|
|
3258
|
+
if self.path == "/v1/operations/projects":
|
|
3259
|
+
try:
|
|
3260
|
+
self._send(200, operations_projection(self.server.data_root), initialize(self.server.data_root).instance_id) # type: ignore[attr-defined]
|
|
3261
|
+
except ServerConfigurationError:
|
|
3262
|
+
self._send(503, {"error": "operations projection unavailable"})
|
|
3263
|
+
return
|
|
3264
|
+
if request.path == "/" or request.path.startswith("/api/") or request.path.startswith("/assets/") or request.path in {"/health", "/favicon.ico", "/apple-touch-icon.png", "/apple-touch-icon-precomposed.png"}:
|
|
3265
|
+
self._delegate_dashboard("do_GET")
|
|
3266
|
+
return
|
|
3267
|
+
if self.path not in {"/healthz", "/readyz"}:
|
|
3268
|
+
self.send_error(404)
|
|
3269
|
+
return
|
|
3270
|
+
try:
|
|
3271
|
+
report = self._status()
|
|
3272
|
+
except ServerConfigurationError:
|
|
3273
|
+
self._send(503, {"healthy": False, "ready": False})
|
|
3274
|
+
return
|
|
3275
|
+
self._send(200, report, str(report["instance_id"]))
|
|
3276
|
+
|
|
3277
|
+
def do_POST(self) -> None: # noqa: N802
|
|
3278
|
+
if urlsplit(self.path).path.startswith("/api/"):
|
|
3279
|
+
self._delegate_dashboard("do_POST")
|
|
3280
|
+
return
|
|
3281
|
+
if self.path.startswith("/v1/projects/") and self.path.endswith("/submissions"):
|
|
3282
|
+
parts = self.path.split("/")
|
|
3283
|
+
if len(parts) != 5 or not parts[3]:
|
|
3284
|
+
self._send(404, {"error": "not found"})
|
|
3285
|
+
return
|
|
3286
|
+
project_id = parts[3]
|
|
3287
|
+
try:
|
|
3288
|
+
if self.headers.get_content_type() != "application/json":
|
|
3289
|
+
raise submission_service.SubmissionError("UNSUPPORTED_MEDIA_TYPE", 415)
|
|
3290
|
+
length = int(self.headers.get("Content-Length", "-1"))
|
|
3291
|
+
if not 0 < length <= 131072:
|
|
3292
|
+
raise submission_service.SubmissionError("PAYLOAD_TOO_LARGE", 413)
|
|
3293
|
+
raw = self.rfile.read(length)
|
|
3294
|
+
if b"\0" in raw:
|
|
3295
|
+
raise submission_service.SubmissionError("MALFORMED_REQUEST")
|
|
3296
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
3297
|
+
authorization = self.headers.get("Authorization", "")
|
|
3298
|
+
token = authorization[7:] if authorization.startswith("Bearer ") else None
|
|
3299
|
+
# CLI uses this same authenticated HTTP boundary, but the
|
|
3300
|
+
# durable receipt must retain the original adapter. It is
|
|
3301
|
+
# observational provenance only: callers cannot select an
|
|
3302
|
+
# execution implementation through this header.
|
|
3303
|
+
transport = self.headers.get("EP-Submission-Transport", "HTTP")
|
|
3304
|
+
with sqlite3.connect(self.server.data_root / SERVER_DATABASE_FILENAME) as connection: # type: ignore[attr-defined]
|
|
3305
|
+
if _authenticated_consumer(connection, token, project_id) is None:
|
|
3306
|
+
raise submission_service.SubmissionError("UNAUTHENTICATED", 401)
|
|
3307
|
+
request = submission_service.request_from_mapping(project_id, payload, transport=transport)
|
|
3308
|
+
result = submission_service.submit(connection, request)
|
|
3309
|
+
self._send(200, result.to_dict(), initialize(self.server.data_root).instance_id) # type: ignore[attr-defined]
|
|
3310
|
+
except UnicodeDecodeError:
|
|
3311
|
+
self._send(400, {"error": "MALFORMED_REQUEST"})
|
|
3312
|
+
except json.JSONDecodeError:
|
|
3313
|
+
self._send(400, {"error": "MALFORMED_REQUEST"})
|
|
3314
|
+
except submission_service.SubmissionError as error:
|
|
3315
|
+
self._send(error.status, {"error": error.code})
|
|
3316
|
+
return
|
|
3317
|
+
routes = {"/v1/agent/pair": agent_trust.pair, "/v1/agent/register": agent_trust.register, "/v1/agent/heartbeat": agent_trust.heartbeat, "/v1/agent/attachment": agent_trust.register_attachment}
|
|
3318
|
+
action = routes.get(self.path)
|
|
3319
|
+
if action is None:
|
|
3320
|
+
self.send_error(404)
|
|
3321
|
+
return
|
|
3322
|
+
try:
|
|
3323
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
3324
|
+
if not 0 < length <= 262144:
|
|
3325
|
+
raise agent_trust.AgentTrustError("request body is invalid")
|
|
3326
|
+
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
3327
|
+
authorization = self.headers.get("Authorization", "")
|
|
3328
|
+
token = authorization.removeprefix("Bearer ") if authorization.startswith("Bearer ") else None
|
|
3329
|
+
with sqlite3.connect(self.server.data_root / SERVER_DATABASE_FILENAME) as connection: # type: ignore[attr-defined]
|
|
3330
|
+
result = action(connection, body) if action is agent_trust.pair else action(connection, body, token)
|
|
3331
|
+
self._send(200, result, initialize(self.server.data_root).instance_id) # type: ignore[attr-defined]
|
|
3332
|
+
except (ValueError, OSError, json.JSONDecodeError, agent_trust.AgentTrustError):
|
|
3333
|
+
self._send(400 if self.path == "/v1/agent/pair" else 401, {"error": "agent request rejected"})
|
|
3334
|
+
|
|
3335
|
+
def log_message(self, _format: str, *_args: object) -> None:
|
|
3336
|
+
return
|
|
3337
|
+
|
|
3338
|
+
|
|
3339
|
+
def serve(data_root: Path) -> int:
|
|
3340
|
+
relocation = installation_relocation.apply_pending(data_root)
|
|
3341
|
+
imported = central_data_transfer.apply_pending_import(data_root)
|
|
3342
|
+
data_root = data_root.resolve()
|
|
3343
|
+
identity = initialize(data_root)
|
|
3344
|
+
if relocation is not None:
|
|
3345
|
+
_audit_configuration_change(
|
|
3346
|
+
data_root,
|
|
3347
|
+
scope="PLATFORM_DATA",
|
|
3348
|
+
key="location",
|
|
3349
|
+
previous=relocation["previous"],
|
|
3350
|
+
value=relocation["value"],
|
|
3351
|
+
)
|
|
3352
|
+
if imported is not None:
|
|
3353
|
+
_audit_configuration_change(
|
|
3354
|
+
data_root, scope="PLATFORM_DATA", key="import", previous="REPLACED",
|
|
3355
|
+
value=f"{imported['entries']}_ENTRIES",
|
|
3356
|
+
)
|
|
3357
|
+
config = ServerConfiguration.load(data_root)
|
|
3358
|
+
os.environ[SERVER_ENVIRONMENT_DATA_ROOT] = str(data_root.resolve())
|
|
3359
|
+
os.environ[MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT] = config.managed_codex_cli_prefix
|
|
3360
|
+
server = http.server.ThreadingHTTPServer((config.bind_host, config.bind_port), _HealthHandler)
|
|
3361
|
+
server.data_root = data_root.resolve() # type: ignore[attr-defined]
|
|
3362
|
+
server.central_data_transfer_lock = RLock() # type: ignore[attr-defined]
|
|
3363
|
+
server.central_data_transfer_active = False # type: ignore[attr-defined]
|
|
3364
|
+
# Lifecycle composition is intentionally lazy: read-only Server import
|
|
3365
|
+
# and Console startup must stay independent of retired watcher modules.
|
|
3366
|
+
from .lifecycle_worker import LifecycleWorker
|
|
3367
|
+
|
|
3368
|
+
worker = LifecycleWorker(data_root)
|
|
3369
|
+
# The File Inbox is an installed Server child, not a Dashboard or
|
|
3370
|
+
# checkout-owned watcher. Its heartbeat is the source for its platform
|
|
3371
|
+
# component health; a prior successful file is never treated as liveness.
|
|
3372
|
+
inbox_service = file_inbox.FileInboxService(
|
|
3373
|
+
data_root / FILE_INBOX_DIRECTORY,
|
|
3374
|
+
admission=lambda envelope, receipt_id, received_at: _admit_server_owned_file_inbox(
|
|
3375
|
+
data_root, envelope, receipt_id, received_at,
|
|
3376
|
+
),
|
|
3377
|
+
)
|
|
3378
|
+
dependabot_service = dependabot_producer.DependabotService(
|
|
3379
|
+
data_root,
|
|
3380
|
+
event=lambda event, context: log_event(
|
|
3381
|
+
component_logger(
|
|
3382
|
+
data_root,
|
|
3383
|
+
"dependabot_producer",
|
|
3384
|
+
central_database=data_root / SERVER_DATABASE_FILENAME,
|
|
3385
|
+
),
|
|
3386
|
+
logging.INFO if event == "dependabot_submission_admitted" else logging.WARNING,
|
|
3387
|
+
event,
|
|
3388
|
+
context=context,
|
|
3389
|
+
),
|
|
3390
|
+
)
|
|
3391
|
+
server.lifecycle_worker = worker # type: ignore[attr-defined]
|
|
3392
|
+
server.inbox_service = inbox_service # type: ignore[attr-defined]
|
|
3393
|
+
server.dependabot_service = dependabot_service # type: ignore[attr-defined]
|
|
3394
|
+
_write_json(data_root / SERVER_RUNTIME_FILENAME, {"pid": os.getpid(), "instance_id": identity.instance_id, "started_at": _utcnow()})
|
|
3395
|
+
def stop(_signum: int, _frame: object) -> None:
|
|
3396
|
+
# ``shutdown`` must run outside the serve_forever thread.
|
|
3397
|
+
import threading
|
|
3398
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
3399
|
+
signal.signal(signal.SIGTERM, stop)
|
|
3400
|
+
signal.signal(signal.SIGINT, stop)
|
|
3401
|
+
worker.start()
|
|
3402
|
+
if not worker.wait_until_running():
|
|
3403
|
+
worker.stop()
|
|
3404
|
+
server.server_close()
|
|
3405
|
+
raise RuntimeError("Lifecycle Worker did not become ready.")
|
|
3406
|
+
inbox_service.start()
|
|
3407
|
+
dependabot_service.start()
|
|
3408
|
+
# A fresh installation must have operational evidence before its first
|
|
3409
|
+
# submission. These are genuine Server lifecycle events, persisted in
|
|
3410
|
+
# the same CENTRAL store that backs the Console's combined log table.
|
|
3411
|
+
for definition in PLATFORM_COMPONENTS:
|
|
3412
|
+
log_event(
|
|
3413
|
+
component_logger(
|
|
3414
|
+
data_root, definition.id,
|
|
3415
|
+
central_database=data_root / SERVER_DATABASE_FILENAME,
|
|
3416
|
+
),
|
|
3417
|
+
logging.INFO,
|
|
3418
|
+
definition.startup_event,
|
|
3419
|
+
context={"target_component": definition.id},
|
|
3420
|
+
)
|
|
3421
|
+
try:
|
|
3422
|
+
server.serve_forever()
|
|
3423
|
+
finally:
|
|
3424
|
+
dependabot_service.stop()
|
|
3425
|
+
inbox_service.stop()
|
|
3426
|
+
worker.stop()
|
|
3427
|
+
server.server_close()
|
|
3428
|
+
(data_root / SERVER_RUNTIME_FILENAME).unlink(missing_ok=True)
|
|
3429
|
+
return 0
|
|
3430
|
+
|
|
3431
|
+
|
|
3432
|
+
def start(data_root: Path) -> dict[str, object]:
|
|
3433
|
+
current = status(data_root)
|
|
3434
|
+
if current["running"]:
|
|
3435
|
+
return current
|
|
3436
|
+
# The installed entrypoint supplies the interpreter. Run from the
|
|
3437
|
+
# installation-owned data root and discard Python import overrides so a
|
|
3438
|
+
# caller's checkout can never become the child Server's import authority.
|
|
3439
|
+
runtime_root = data_root.resolve()
|
|
3440
|
+
configuration = ServerConfiguration.load(runtime_root)
|
|
3441
|
+
# npm is the preserved managed-runtime installer. These are fixed host
|
|
3442
|
+
# tool directories, never a provider-executable fallback or caller PATH.
|
|
3443
|
+
environment = {
|
|
3444
|
+
"PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
3445
|
+
"PYTHONNOUSERSITE": "1",
|
|
3446
|
+
"PYTHONSAFEPATH": "1",
|
|
3447
|
+
MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT: configuration.managed_codex_cli_prefix,
|
|
3448
|
+
SERVER_ENVIRONMENT_DATA_ROOT: str(runtime_root),
|
|
3449
|
+
}
|
|
3450
|
+
if home := os.environ.get("HOME"):
|
|
3451
|
+
environment["HOME"] = home
|
|
3452
|
+
# Unit tests exercise the lifecycle from an unpackaged source tree. This
|
|
3453
|
+
# explicit test-only bridge is never inherited by an installed process.
|
|
3454
|
+
if "unittest" in sys.argv[0]:
|
|
3455
|
+
environment["PYTHONPATH"] = str(Path(__file__).resolve().parents[1])
|
|
3456
|
+
child = subprocess.Popen([sys.executable, "-m", "engineering_platform.server", "serve", "--data-root", str(runtime_root)], cwd=str(runtime_root), env=environment, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # nosec B603
|
|
3457
|
+
_CHILDREN[child.pid] = child
|
|
3458
|
+
for _ in range(40):
|
|
3459
|
+
time.sleep(0.05)
|
|
3460
|
+
current = status(data_root)
|
|
3461
|
+
if current["running"]:
|
|
3462
|
+
return current
|
|
3463
|
+
raise RuntimeError("EP Server did not become ready.")
|
|
3464
|
+
|
|
3465
|
+
|
|
3466
|
+
def stop(data_root: Path) -> dict[str, object]:
|
|
3467
|
+
runtime = _runtime(data_root)
|
|
3468
|
+
if runtime and _alive(runtime.get("pid")):
|
|
3469
|
+
os.kill(int(runtime["pid"]), signal.SIGTERM)
|
|
3470
|
+
child = _CHILDREN.pop(int(runtime["pid"]), None)
|
|
3471
|
+
if child is not None:
|
|
3472
|
+
try:
|
|
3473
|
+
child.wait(timeout=2)
|
|
3474
|
+
except subprocess.TimeoutExpired:
|
|
3475
|
+
pass
|
|
3476
|
+
for _ in range(40):
|
|
3477
|
+
time.sleep(0.05)
|
|
3478
|
+
if not _alive(runtime["pid"]):
|
|
3479
|
+
break
|
|
3480
|
+
(data_root / SERVER_RUNTIME_FILENAME).unlink(missing_ok=True)
|
|
3481
|
+
return status(data_root)
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
def health(data_root: Path) -> dict[str, object]:
|
|
3485
|
+
result = status(data_root)
|
|
3486
|
+
if not result["running"]:
|
|
3487
|
+
return {**result, "healthy": False, "ready": False}
|
|
3488
|
+
bind = result["bind"]
|
|
3489
|
+
try:
|
|
3490
|
+
# Server configuration permits loopback host only.
|
|
3491
|
+
with urlopen(f"http://{bind['host']}:{bind['port']}/health", timeout=1) as response: # nosec B310
|
|
3492
|
+
response.read()
|
|
3493
|
+
return {**result, "healthy": True, "ready": True}
|
|
3494
|
+
except (URLError, OSError):
|
|
3495
|
+
return {**result, "healthy": False, "ready": False}
|
|
3496
|
+
|
|
3497
|
+
|
|
3498
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
3499
|
+
parser = argparse.ArgumentParser(prog="engineering-platform-server", description="Manage the standalone Engineering Platform Server foundation")
|
|
3500
|
+
parser.add_argument("command", choices=("init", "start", "serve", "stop", "status", "health", "service-install", "service-uninstall", "relay-install", "relay-uninstall", "pairing-create", "agent-status", "agent-revoke", "agent-reset", "topology", "submission-diagnose", "bootstrap-topology", "register-topology", "provision-declaration", "issue-consumer-credential", "bind-repository", "rebind-repository", "unbind-repository", "resolve-repository", "register-producer-binding", "list-producer-bindings", "deactivate-producer-binding"))
|
|
3501
|
+
parser.add_argument("--data-root", type=Path, default=default_data_root())
|
|
3502
|
+
parser.add_argument("--bind-host", default="127.0.0.1")
|
|
3503
|
+
parser.add_argument("--bind-port", type=int, default=8765)
|
|
3504
|
+
parser.add_argument("--agent-id")
|
|
3505
|
+
parser.add_argument("--project-id")
|
|
3506
|
+
parser.add_argument("--repository-id")
|
|
3507
|
+
parser.add_argument("--path", type=Path)
|
|
3508
|
+
parser.add_argument("--declaration", type=Path)
|
|
3509
|
+
parser.add_argument("--consumer-id")
|
|
3510
|
+
parser.add_argument("--submission-id")
|
|
3511
|
+
parser.add_argument("--producer-type")
|
|
3512
|
+
parser.add_argument("--external-resource-type")
|
|
3513
|
+
parser.add_argument("--external-resource-identity")
|
|
3514
|
+
parser.add_argument("--binding-id")
|
|
3515
|
+
parser.add_argument("--reason")
|
|
3516
|
+
return parser
|
|
3517
|
+
|
|
3518
|
+
|
|
3519
|
+
def main(argv: list[str] | None = None) -> int:
|
|
3520
|
+
args = build_parser().parse_args(argv)
|
|
3521
|
+
try:
|
|
3522
|
+
if args.command == "init":
|
|
3523
|
+
result = {"instance_id": initialize(args.data_root, bind_host=args.bind_host, bind_port=args.bind_port).instance_id, "initialized": True}
|
|
3524
|
+
elif args.command == "start":
|
|
3525
|
+
initialize(args.data_root)
|
|
3526
|
+
configuration = ServerConfiguration.load(args.data_root)
|
|
3527
|
+
if (configuration.bind_host, configuration.bind_port) != (args.bind_host, args.bind_port):
|
|
3528
|
+
_write_json(args.data_root / SERVER_CONFIGURATION_FILENAME, {
|
|
3529
|
+
"version": configuration.version, "bind_host": args.bind_host,
|
|
3530
|
+
"bind_port": args.bind_port,
|
|
3531
|
+
"managed_codex_cli_prefix": configuration.managed_codex_cli_prefix,
|
|
3532
|
+
})
|
|
3533
|
+
result = start(args.data_root)
|
|
3534
|
+
elif args.command == "serve": return serve(args.data_root)
|
|
3535
|
+
elif args.command == "stop": result = stop(args.data_root)
|
|
3536
|
+
elif args.command == "status": result = status(args.data_root)
|
|
3537
|
+
elif args.command == "health": result = health(args.data_root)
|
|
3538
|
+
elif args.command == "service-install":
|
|
3539
|
+
initialize(args.data_root)
|
|
3540
|
+
result = {"result": "INSTALLED", **server_service.install(args.data_root)}
|
|
3541
|
+
elif args.command == "service-uninstall": result = {"result": "UNINSTALLED", **server_service.uninstall(args.data_root)}
|
|
3542
|
+
elif args.command == "relay-install":
|
|
3543
|
+
initialize(args.data_root)
|
|
3544
|
+
result = {"result": "INSTALLED", **server_relay.install(args.data_root)}
|
|
3545
|
+
elif args.command == "relay-uninstall":
|
|
3546
|
+
result = {"result": "UNINSTALLED", **server_relay.uninstall()}
|
|
3547
|
+
elif args.command == "topology":
|
|
3548
|
+
initialize(args.data_root)
|
|
3549
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3550
|
+
result = project_topology.topology(connection)
|
|
3551
|
+
elif args.command == "submission-diagnose":
|
|
3552
|
+
if not args.submission_id:
|
|
3553
|
+
raise ServerConfigurationError("--submission-id is required for submission diagnostics.")
|
|
3554
|
+
initialize(args.data_root)
|
|
3555
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3556
|
+
row = connection.execute("SELECT s.project_id,s.repository_id,s.state,s.admission,s.transport,s.transport_receipt_id,s.transport_received_at,p.run_id,d.state,d.operator_resolution,p.project_id,p.repository_id FROM ep_submissions s LEFT JOIN ep_receipt_run_provenance p ON p.submission_id=s.submission_id LEFT JOIN ep_parity_lifecycle_dispatches d ON d.run_id=p.run_id WHERE s.submission_id=?", (args.submission_id,)).fetchone()
|
|
3557
|
+
if row is None:
|
|
3558
|
+
raise ServerConfigurationError("UNKNOWN_SUBMISSION")
|
|
3559
|
+
project_id, repository_id, state, admission, transport, receipt_id, received_at, run_id, dispatch_state, resolution, dispatch_project, dispatch_repository = row
|
|
3560
|
+
blocked = connection.execute("SELECT run_id,state FROM ep_parity_lifecycle_dispatches WHERE project_id=? AND submission_id!=? AND state IN ('CLAIMED','RUNNING','BLOCKED','FAILED') AND run_id!=? ORDER BY updated_at LIMIT 1", (project_id, args.submission_id, run_id or "")).fetchone()
|
|
3561
|
+
admission_audit = connection.execute("SELECT 1 FROM ep_submission_events WHERE submission_id=? AND event_kind='ADMISSION_GRANTED'", (args.submission_id,)).fetchone()
|
|
3562
|
+
early = None
|
|
3563
|
+
if run_id:
|
|
3564
|
+
early_path = args.data_root / "artifacts" / "projects" / str(project_id) / "runs" / str(run_id) / "early-runner-failure.json"
|
|
3565
|
+
if early_path.is_file():
|
|
3566
|
+
try:
|
|
3567
|
+
early = json.loads(early_path.read_text(encoding="utf-8"))
|
|
3568
|
+
except (OSError, json.JSONDecodeError):
|
|
3569
|
+
early = {"diagnostic_code": "EARLY_FAILURE_EVIDENCE_UNAVAILABLE"}
|
|
3570
|
+
receipt_complete = transport != "FILE_INBOX" or (isinstance(receipt_id, str) and bool(receipt_id) and isinstance(received_at, str) and bool(received_at))
|
|
3571
|
+
scope_complete = run_id is not None and (dispatch_project, dispatch_repository) == (project_id, repository_id)
|
|
3572
|
+
result = {"submission_id": args.submission_id, "project_id": project_id, "repository_id": repository_id, "submission_state": state, "admission": admission, "run_id": run_id, "dispatch_state": dispatch_state, "operator_resolution": resolution, "transport_provenance": "COMPLETE" if receipt_complete else "INCOMPLETE", "admission_audit_provenance": "PRESENT" if admission_audit else "UNAVAILABLE", "receipt_run_provenance": "PRESENT" if run_id else "UNAVAILABLE", "dispatch_scope_provenance": "COMPLETE" if scope_complete else "UNAVAILABLE", "lane_blocker": {"run_id": blocked[0], "state": blocked[1]} if blocked else None, "early_failure": early, "worker_eligible": state == "QUEUED" and admission == "ADMITTED" and blocked is None}
|
|
3573
|
+
elif args.command == "register-topology":
|
|
3574
|
+
if args.declaration is None:
|
|
3575
|
+
raise ServerConfigurationError("--declaration is required for explicit topology registration.")
|
|
3576
|
+
initialize(args.data_root)
|
|
3577
|
+
try:
|
|
3578
|
+
declaration = json.loads(args.declaration.read_text(encoding="utf-8"))
|
|
3579
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
3580
|
+
raise ServerConfigurationError("REPOSITORY_DECLARATION_UNREADABLE") from error
|
|
3581
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3582
|
+
result = project_topology.register_server_local_topology(connection, declaration=declaration)
|
|
3583
|
+
elif args.command == "bootstrap-topology":
|
|
3584
|
+
if not args.project_id or not args.repository_id:
|
|
3585
|
+
raise ServerConfigurationError("--project-id and --repository-id are required for topology bootstrap.")
|
|
3586
|
+
initialize(args.data_root)
|
|
3587
|
+
declaration = {"schema_version": "1.0", "project": {"id": args.project_id, "authority_repository_id": args.repository_id}, "repository": {"id": args.repository_id, "role": "authority"}, "validation": {"kind": "none"}}
|
|
3588
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3589
|
+
result = project_topology.register_server_local_topology(connection, declaration=declaration)
|
|
3590
|
+
elif args.command == "issue-consumer-credential":
|
|
3591
|
+
if not args.project_id or not args.consumer_id:
|
|
3592
|
+
raise ServerConfigurationError("--project-id and --consumer-id are required for credential issuance.")
|
|
3593
|
+
initialize(args.data_root)
|
|
3594
|
+
from .submission_service import issue_consumer_credential
|
|
3595
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3596
|
+
result = issue_consumer_credential(connection, consumer_id=args.consumer_id, project_id=args.project_id)
|
|
3597
|
+
elif args.command == "register-producer-binding":
|
|
3598
|
+
if not all((args.producer_type, args.external_resource_type, args.external_resource_identity, args.project_id, args.repository_id, args.reason)):
|
|
3599
|
+
raise ServerConfigurationError("--producer-type, --external-resource-type, --external-resource-identity, --project-id, --repository-id and --reason are required for producer binding registration.")
|
|
3600
|
+
initialize(args.data_root)
|
|
3601
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3602
|
+
binding = external_producer_binding.register(
|
|
3603
|
+
connection,
|
|
3604
|
+
data_root=args.data_root,
|
|
3605
|
+
producer_type=args.producer_type,
|
|
3606
|
+
external_resource_type=args.external_resource_type,
|
|
3607
|
+
external_resource_identity=args.external_resource_identity,
|
|
3608
|
+
project_id=args.project_id,
|
|
3609
|
+
repository_id=args.repository_id,
|
|
3610
|
+
reason=args.reason,
|
|
3611
|
+
)
|
|
3612
|
+
result = {"binding_id": binding.binding_id, "project_id": binding.project_id, "repository_id": binding.repository_id, "version": binding.version, "result": "REGISTERED"}
|
|
3613
|
+
elif args.command == "list-producer-bindings":
|
|
3614
|
+
initialize(args.data_root)
|
|
3615
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3616
|
+
result = {"bindings": external_producer_binding.list_bindings(connection, data_root=args.data_root)}
|
|
3617
|
+
elif args.command == "deactivate-producer-binding":
|
|
3618
|
+
if not args.binding_id or not args.reason:
|
|
3619
|
+
raise ServerConfigurationError("--binding-id and --reason are required for producer binding deactivation.")
|
|
3620
|
+
initialize(args.data_root)
|
|
3621
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3622
|
+
binding = external_producer_binding.deactivate(connection, data_root=args.data_root, binding_id=args.binding_id, reason=args.reason)
|
|
3623
|
+
result = {"binding_id": binding.binding_id, "project_id": binding.project_id, "repository_id": binding.repository_id, "version": binding.version, "result": "DEACTIVATED"}
|
|
3624
|
+
elif args.command == "provision-declaration":
|
|
3625
|
+
if not args.project_id or not args.repository_id or args.path is None:
|
|
3626
|
+
raise ServerConfigurationError("--project-id, --repository-id and --path are required for declaration provisioning.")
|
|
3627
|
+
initialize(args.data_root)
|
|
3628
|
+
from .repository_attachment import config_path, load_repository_attachment, parse_repository_attachment
|
|
3629
|
+
root = args.path.resolve(strict=True)
|
|
3630
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3631
|
+
row = connection.execute("SELECT attachment_contract FROM ep_repository_registrations WHERE project_id=? AND repository_id=?", (args.project_id, args.repository_id)).fetchone()
|
|
3632
|
+
if row is None:
|
|
3633
|
+
raise ServerConfigurationError("CENTRAL_REPOSITORY_NOT_REGISTERED")
|
|
3634
|
+
declaration = json.loads(str(row[0]))
|
|
3635
|
+
parse_repository_attachment(declaration)
|
|
3636
|
+
target = config_path(root)
|
|
3637
|
+
if target.exists():
|
|
3638
|
+
existing = load_repository_attachment(root)
|
|
3639
|
+
if (existing.project_id, existing.repository_id) != (args.project_id, args.repository_id):
|
|
3640
|
+
raise ServerConfigurationError("REPOSITORY_DECLARATION_CONFLICT")
|
|
3641
|
+
else:
|
|
3642
|
+
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
3643
|
+
target.write_text(json.dumps(declaration, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
3644
|
+
target.chmod(0o600)
|
|
3645
|
+
result = {"project_id": args.project_id, "repository_id": args.repository_id, "path": str(target), "result": "PROVISIONED"}
|
|
3646
|
+
elif args.command in {"bind-repository", "rebind-repository", "unbind-repository", "resolve-repository"}:
|
|
3647
|
+
if not args.project_id or not args.repository_id:
|
|
3648
|
+
raise ServerConfigurationError("--project-id and --repository-id are required for local binding commands.")
|
|
3649
|
+
initialize(args.data_root)
|
|
3650
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3651
|
+
if args.command in {"bind-repository", "rebind-repository"}:
|
|
3652
|
+
if args.path is None:
|
|
3653
|
+
raise ServerConfigurationError("--path is required when binding a repository.")
|
|
3654
|
+
binding = local_repository_binding.bind_local_repository(connection, project_id=args.project_id, repository_id=args.repository_id, local_root=args.path, data_root=args.data_root, rebind=args.command == "rebind-repository")
|
|
3655
|
+
result = {"project_id": binding.project_id, "repository_id": binding.repository_id, "state": binding.state}
|
|
3656
|
+
elif args.command == "unbind-repository":
|
|
3657
|
+
local_repository_binding.unbind_local_repository(connection, project_id=args.project_id, repository_id=args.repository_id)
|
|
3658
|
+
result = {"project_id": args.project_id, "repository_id": args.repository_id, "state": "UNBOUND"}
|
|
3659
|
+
else:
|
|
3660
|
+
binding = local_repository_binding.resolve_execution_repository(connection, project_id=args.project_id, repository_id=args.repository_id, data_root=args.data_root)
|
|
3661
|
+
result = {"project_id": binding.project_id, "repository_id": binding.repository_id, "state": binding.state}
|
|
3662
|
+
else:
|
|
3663
|
+
if not args.agent_id:
|
|
3664
|
+
raise ServerConfigurationError("--agent-id is required for Agent lifecycle commands.")
|
|
3665
|
+
initialize(args.data_root)
|
|
3666
|
+
with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection:
|
|
3667
|
+
if args.command == "pairing-create": result = agent_trust.create_pairing_code(connection, args.agent_id)
|
|
3668
|
+
elif args.command == "agent-status": result = agent_trust.registration_status(connection, args.agent_id)
|
|
3669
|
+
elif args.command == "agent-revoke": result = {"agent_id": args.agent_id, "revoked": agent_trust.revoke(connection, args.agent_id)}
|
|
3670
|
+
else: result = {"agent_id": args.agent_id, "reset": agent_trust.reset(connection, args.agent_id)}
|
|
3671
|
+
except (OSError, RuntimeError, PermissionError, ServerConfigurationError, local_repository_binding.LocalRepositoryBindingError, external_producer_binding.ProducerBindingError) as error:
|
|
3672
|
+
print(json.dumps({"error": str(error), "ready": False}, sort_keys=True))
|
|
3673
|
+
return 2
|
|
3674
|
+
print(json.dumps(result, sort_keys=True))
|
|
3675
|
+
return 0 if result.get("ready", True) else 1
|
|
3676
|
+
|
|
3677
|
+
|
|
3678
|
+
if __name__ == "__main__":
|
|
3679
|
+
raise SystemExit(main())
|