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.
Files changed (130) hide show
  1. engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
  2. engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
  3. engineering_platform/__init__.py +1 -0
  4. engineering_platform/__main__.py +7 -0
  5. engineering_platform/agent_state.py +530 -0
  6. engineering_platform/agent_trust.py +174 -0
  7. engineering_platform/assets/dashboard.css +1317 -0
  8. engineering_platform/assets/dashboard.js +8534 -0
  9. engineering_platform/assets/dashboard_locales.mjs +4049 -0
  10. engineering_platform/assets/dashboard_status_store.mjs +41 -0
  11. engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  12. engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  13. engineering_platform/assets/operations-console/icon-dark.png +0 -0
  14. engineering_platform/assets/operations-console/icon-light.png +0 -0
  15. engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  16. engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
  17. engineering_platform/capability_preflight.py +285 -0
  18. engineering_platform/capability_review.py +261 -0
  19. engineering_platform/central_data_transfer.py +195 -0
  20. engineering_platform/central_database.py +245 -0
  21. engineering_platform/central_store_migration.py +1672 -0
  22. engineering_platform/codex_capacity.py +81 -0
  23. engineering_platform/codex_chat.py +226 -0
  24. engineering_platform/codex_observability.py +153 -0
  25. engineering_platform/component_lock.py +40 -0
  26. engineering_platform/component_logging.py +420 -0
  27. engineering_platform/console_presentation.py +14 -0
  28. engineering_platform/console_route_ownership.py +83 -0
  29. engineering_platform/contracts/__init__.py +38 -0
  30. engineering_platform/contracts/ep_consumer.py +391 -0
  31. engineering_platform/contracts/models.py +105 -0
  32. engineering_platform/contracts/projection.py +401 -0
  33. engineering_platform/dashboard_browser_validation.py +206 -0
  34. engineering_platform/dashboard_state.py +630 -0
  35. engineering_platform/dashboard_supervisor.swift +105 -0
  36. engineering_platform/dashboard_translation.py +129 -0
  37. engineering_platform/dependabot_producer.py +349 -0
  38. engineering_platform/drift_diagnostics.py +144 -0
  39. engineering_platform/emergency_recovery.py +268 -0
  40. engineering_platform/engineering_memory.py +139 -0
  41. engineering_platform/ep_consumer_credentials.py +473 -0
  42. engineering_platform/evidence_projection.py +213 -0
  43. engineering_platform/execution_activity.py +218 -0
  44. engineering_platform/execution_context.py +132 -0
  45. engineering_platform/execution_errors.py +42 -0
  46. engineering_platform/execution_evidence.py +24 -0
  47. engineering_platform/execution_executor.py +730 -0
  48. engineering_platform/execution_finalization.py +44 -0
  49. engineering_platform/execution_host.py +3306 -0
  50. engineering_platform/execution_lease.py +365 -0
  51. engineering_platform/execution_lifecycle.py +447 -0
  52. engineering_platform/execution_models.py +43 -0
  53. engineering_platform/execution_readiness.py +166 -0
  54. engineering_platform/execution_reporting.py +1607 -0
  55. engineering_platform/execution_repository.py +253 -0
  56. engineering_platform/execution_timeout_policy.py +56 -0
  57. engineering_platform/execution_timing.py +440 -0
  58. engineering_platform/execution_transaction.py +28 -0
  59. engineering_platform/external_producer_binding.py +235 -0
  60. engineering_platform/file_inbox.py +249 -0
  61. engineering_platform/forensic_attribution.py +338 -0
  62. engineering_platform/forensic_attribution_v2.py +134 -0
  63. engineering_platform/forensic_delta.py +299 -0
  64. engineering_platform/golden_scenario.py +63 -0
  65. engineering_platform/historical_dashboard_configuration.py +171 -0
  66. engineering_platform/host_admin.py +199 -0
  67. engineering_platform/host_preflight.py +231 -0
  68. engineering_platform/installation_relocation.py +122 -0
  69. engineering_platform/investigation_ledger.py +89 -0
  70. engineering_platform/legacy_inbox_migration.py +79 -0
  71. engineering_platform/lifecycle_worker.py +223 -0
  72. engineering_platform/live_status.py +267 -0
  73. engineering_platform/local_api.py +209 -0
  74. engineering_platform/local_api_keychain.py +51 -0
  75. engineering_platform/local_repository_binding.py +138 -0
  76. engineering_platform/managed_autonomy.py +509 -0
  77. engineering_platform/managed_codex_runtime.py +105 -0
  78. engineering_platform/parity_context.py +203 -0
  79. engineering_platform/parity_lifecycle_dispatcher.py +488 -0
  80. engineering_platform/platform_admin.py +13 -0
  81. engineering_platform/platform_api.py +428 -0
  82. engineering_platform/platform_bootstrap.py +385 -0
  83. engineering_platform/platform_components.py +65 -0
  84. engineering_platform/platform_version.py +171 -0
  85. engineering_platform/pr_check_repair.py +276 -0
  86. engineering_platform/pr_evidence_backfill.py +278 -0
  87. engineering_platform/producer.py +209 -0
  88. engineering_platform/project_agent.py +366 -0
  89. engineering_platform/project_agent_service.py +244 -0
  90. engineering_platform/project_topology.py +126 -0
  91. engineering_platform/prompt_history.py +591 -0
  92. engineering_platform/provider_context.py +136 -0
  93. engineering_platform/provider_context_benchmark.py +41 -0
  94. engineering_platform/provider_context_scope.py +90 -0
  95. engineering_platform/provider_interruption.py +168 -0
  96. engineering_platform/provider_process_identity.py +80 -0
  97. engineering_platform/provider_readiness.py +138 -0
  98. engineering_platform/provider_recovery.py +647 -0
  99. engineering_platform/provider_usage.py +497 -0
  100. engineering_platform/providers.py +471 -0
  101. engineering_platform/qualification.py +220 -0
  102. engineering_platform/recommendation_handoff.py +238 -0
  103. engineering_platform/report_analysis.py +193 -0
  104. engineering_platform/repository_attachment.py +171 -0
  105. engineering_platform/repository_handoff.py +95 -0
  106. engineering_platform/resources.py +38 -0
  107. engineering_platform/reviewer_evidence.py +70 -0
  108. engineering_platform/schemas/repository-attachment.schema.json +61 -0
  109. engineering_platform/server.py +3679 -0
  110. engineering_platform/server_console_services.py +2024 -0
  111. engineering_platform/server_relay.py +172 -0
  112. engineering_platform/server_service.py +122 -0
  113. engineering_platform/status_model.py +135 -0
  114. engineering_platform/status_reconciliation.py +34 -0
  115. engineering_platform/storage.py +2440 -0
  116. engineering_platform/submission_cli.py +77 -0
  117. engineering_platform/submission_intake.py +45 -0
  118. engineering_platform/submission_service.py +317 -0
  119. engineering_platform/telemetry.py +951 -0
  120. engineering_platform/templates/workspace-config.json +25 -0
  121. engineering_platform/validation_identity.py +50 -0
  122. engineering_platform/validation_profile.py +211 -0
  123. engineering_platform/workspace_preflight.py +263 -0
  124. engineering_platform/worktree_provenance.py +147 -0
  125. engineering_platform/worktree_tooling.py +18 -0
  126. engineering_platform-2.2.0.dist-info/METADATA +18 -0
  127. engineering_platform-2.2.0.dist-info/RECORD +130 -0
  128. engineering_platform-2.2.0.dist-info/WHEEL +5 -0
  129. engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
  130. engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
1
+ """macOS per-user LaunchAgent lifecycle for the EP Project Agent (B6B).
2
+
3
+ This module owns local packaging and supervision only. It deliberately does
4
+ not define Agent pairing, authentication, registration, or repository access.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import asdict, dataclass
9
+ from datetime import datetime, timezone
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import platform
14
+ import shutil
15
+ import subprocess
16
+ import time
17
+ from typing import Callable, Mapping, Sequence
18
+
19
+
20
+ LABEL = "com.engineeringplatform.project-agent"
21
+ CONFIG_VERSION = 1
22
+ DEFAULT_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
23
+ PRODUCT = "Engineering Platform"
24
+ COMPONENT_ROLE = "project-agent"
25
+
26
+
27
+ class AgentServiceError(ValueError):
28
+ """Raised when a bounded Agent lifecycle operation cannot proceed safely."""
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class AgentPaths:
33
+ config_dir: Path
34
+ state_dir: Path
35
+ log_dir: Path
36
+ launch_agents_dir: Path
37
+
38
+ @property
39
+ def config_path(self) -> Path:
40
+ return self.config_dir / "config.json"
41
+
42
+ @property
43
+ def plist_path(self) -> Path:
44
+ return self.launch_agents_dir / f"{LABEL}.plist"
45
+
46
+ @property
47
+ def stdout_log(self) -> Path:
48
+ # Launchd output is a bounded process diagnostic, never a second EP
49
+ # operational log authority. The unpaired Project Agent has no
50
+ # CENTRAL writer of its own, so discard its idle-loop output instead
51
+ # of retaining a persistent per-agent logfile.
52
+ return Path("/dev/null")
53
+
54
+ @property
55
+ def stderr_log(self) -> Path:
56
+ return Path("/dev/null")
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class AgentConfiguration:
61
+ version: int
62
+ toolchain_paths: tuple[str, ...] = ()
63
+ b6a: Mapping[str, str] | None = None
64
+
65
+ @classmethod
66
+ def load(cls, path: Path) -> "AgentConfiguration":
67
+ try:
68
+ raw = json.loads(path.read_text(encoding="utf-8"))
69
+ except (OSError, json.JSONDecodeError) as error:
70
+ raise AgentServiceError("Project Agent configuration is unavailable.") from error
71
+ if not isinstance(raw, dict) or set(raw) != {"version", "toolchain_paths", "b6a"}:
72
+ raise AgentServiceError("Project Agent configuration has unexpected fields.")
73
+ paths, b6a = raw["toolchain_paths"], raw["b6a"]
74
+ if raw["version"] != CONFIG_VERSION or not isinstance(paths, list) or not all(isinstance(item, str) and Path(item).is_absolute() for item in paths):
75
+ raise AgentServiceError("Project Agent configuration is invalid.")
76
+ if b6a is not None:
77
+ allowed = {"server_endpoint", "paired_agent_identity", "credential_reference", "protocol_version"}
78
+ if not isinstance(b6a, dict) or not set(b6a).issubset(allowed) or not all(isinstance(key, str) and isinstance(value, str) and value for key, value in b6a.items()):
79
+ raise AgentServiceError("Project Agent B6A extension configuration is invalid.")
80
+ return cls(CONFIG_VERSION, tuple(paths), b6a)
81
+
82
+ def payload(self) -> dict[str, object]:
83
+ return {"version": self.version, "toolchain_paths": list(self.toolchain_paths), "b6a": dict(self.b6a) if self.b6a else None}
84
+
85
+
86
+ def default_paths(home: Path | None = None) -> AgentPaths:
87
+ root = (home or Path.home()).expanduser()
88
+ return AgentPaths(
89
+ root / "Library" / "Application Support" / PRODUCT / "Project Agent",
90
+ root / "Library" / "Caches" / PRODUCT / "Project Agent",
91
+ root / "Library" / "Logs" / PRODUCT / "Project Agent",
92
+ root / "Library" / "LaunchAgents",
93
+ )
94
+
95
+
96
+ def runtime_path(configuration: AgentConfiguration) -> str:
97
+ return ":".join((*configuration.toolchain_paths, DEFAULT_PATH))
98
+
99
+
100
+ def artifact_metadata(executable: Path) -> dict[str, str | None]:
101
+ """Metadata envelope for a future qualified artifact publication process."""
102
+ from importlib.metadata import version
103
+ try:
104
+ installed_version = version("engineering-platform")
105
+ except Exception: # pragma: no cover - source-tree developer invocation
106
+ installed_version = "unknown"
107
+ return {"product": PRODUCT, "component_role": COMPONENT_ROLE, "version": installed_version, "source_repository": "pcvantol/engineering-platform", "source_sha": None, "artifact_identity": str(executable), "artifact_sha256": None, "platform": "macos", "architecture": platform.machine() or None, "compatibility": "macos user LaunchAgent", "release_channel": None, "qualification_reference": None, "signature_attestation": None}
108
+
109
+
110
+ def _write_json(path: Path, payload: object) -> None:
111
+ temporary = path.with_suffix(path.suffix + ".tmp")
112
+ temporary.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
113
+ temporary.chmod(0o600)
114
+ os.replace(temporary, path)
115
+ path.chmod(0o600)
116
+
117
+
118
+ def initialize(paths: AgentPaths) -> AgentConfiguration:
119
+ for directory in (paths.config_dir, paths.state_dir, paths.log_dir, paths.launch_agents_dir):
120
+ directory.mkdir(parents=True, exist_ok=True)
121
+ for directory in (paths.config_dir, paths.state_dir, paths.log_dir):
122
+ directory.chmod(0o700)
123
+ if not paths.config_path.exists():
124
+ _write_json(paths.config_path, AgentConfiguration(CONFIG_VERSION).payload())
125
+ return AgentConfiguration.load(paths.config_path)
126
+
127
+
128
+ def resolve_installed_executable(candidate: str | Path | None = None) -> Path:
129
+ raw = str(candidate) if candidate else shutil.which("engineering-project-agent")
130
+ if not raw:
131
+ raise AgentServiceError("The installed engineering-project-agent executable was not found on PATH.")
132
+ executable = Path(raw).expanduser().absolute()
133
+ if not executable.is_file() or not os.access(executable, os.X_OK):
134
+ raise AgentServiceError("The Project Agent executable is not an executable installed artifact.")
135
+ return executable
136
+
137
+
138
+ def plist_payload(paths: AgentPaths, executable: Path, configuration: AgentConfiguration) -> dict[str, object]:
139
+ return {
140
+ "Label": LABEL,
141
+ "ProgramArguments": [str(executable), "service", "run", "--config", str(paths.config_path)],
142
+ "RunAtLoad": True,
143
+ "KeepAlive": {"SuccessfulExit": False},
144
+ "ProcessType": "Background",
145
+ "EnvironmentVariables": {"PATH": runtime_path(configuration), "ENGINEERING_PLATFORM_AGENT_CONFIG": str(paths.config_path)},
146
+ "StandardOutPath": str(paths.stdout_log),
147
+ "StandardErrorPath": str(paths.stderr_log),
148
+ }
149
+
150
+
151
+ def plist_text(payload: Mapping[str, object]) -> str:
152
+ import plistlib
153
+ return plistlib.dumps(dict(payload), fmt=plistlib.FMT_XML, sort_keys=True).decode("utf-8")
154
+
155
+
156
+ def write_plist(paths: AgentPaths, executable: Path, configuration: AgentConfiguration) -> Path:
157
+ content = plist_text(plist_payload(paths, executable, configuration))
158
+ if any(word in content.lower() for word in ("secret", "password", "token=")):
159
+ raise AgentServiceError("Refusing to write a plist containing secret material.")
160
+ temporary = paths.plist_path.with_suffix(".plist.tmp")
161
+ temporary.write_text(content, encoding="utf-8")
162
+ temporary.chmod(0o644)
163
+ os.replace(temporary, paths.plist_path)
164
+ paths.plist_path.chmod(0o644)
165
+ return paths.plist_path
166
+
167
+
168
+ Runner = Callable[[Sequence[str]], subprocess.CompletedProcess[str]]
169
+
170
+
171
+ def _launchctl(arguments: Sequence[str], runner: Runner | None = None) -> subprocess.CompletedProcess[str]:
172
+ if platform.system() != "Darwin":
173
+ raise AgentServiceError("Project Agent LaunchAgent lifecycle is supported only on macOS.")
174
+ return (runner or (lambda command: subprocess.run(command, capture_output=True, text=True, check=False)))(["launchctl", *arguments])
175
+
176
+
177
+ def _domain() -> str:
178
+ return f"gui/{os.getuid()}"
179
+
180
+
181
+ def install(paths: AgentPaths | None = None, *, executable: str | Path | None = None, runner: Runner | None = None) -> dict[str, object]:
182
+ locations = paths or default_paths()
183
+ configuration = initialize(locations)
184
+ installed_executable = resolve_installed_executable(executable)
185
+ plist = write_plist(locations, installed_executable, configuration)
186
+ result = _launchctl(("bootstrap", _domain(), str(plist)), runner)
187
+ # Already loaded is the expected idempotent repair outcome; do not hide other failures.
188
+ if result.returncode and "service already loaded" not in (result.stderr or "").lower():
189
+ raise AgentServiceError("Unable to bootstrap Project Agent LaunchAgent.")
190
+ return {"state": "installed", "plist": str(plist), "configuration": str(locations.config_path), "artifact": artifact_metadata(installed_executable)}
191
+
192
+
193
+ def start(paths: AgentPaths | None = None, *, runner: Runner | None = None) -> None:
194
+ locations = paths or default_paths()
195
+ if not locations.plist_path.exists():
196
+ raise AgentServiceError("Project Agent is not installed.")
197
+ result = _launchctl(("bootstrap", _domain(), str(locations.plist_path)), runner)
198
+ if result.returncode and "service already loaded" not in (result.stderr or "").lower():
199
+ raise AgentServiceError("Unable to load Project Agent LaunchAgent.")
200
+ result = _launchctl(("kickstart", "-k", f"{_domain()}/{LABEL}"), runner)
201
+ if result.returncode:
202
+ raise AgentServiceError("Unable to start Project Agent LaunchAgent.")
203
+
204
+
205
+ def stop(paths: AgentPaths | None = None, *, runner: Runner | None = None) -> None:
206
+ result = _launchctl(("bootout", f"{_domain()}/{LABEL}"), runner)
207
+ if result.returncode and not _not_loaded(result.stderr):
208
+ raise AgentServiceError("Unable to stop Project Agent LaunchAgent.")
209
+
210
+
211
+ def uninstall(paths: AgentPaths | None = None, *, runner: Runner | None = None) -> None:
212
+ locations = paths or default_paths()
213
+ if locations.plist_path.exists():
214
+ result = _launchctl(("bootout", _domain(), str(locations.plist_path)), runner)
215
+ if result.returncode and not _not_loaded(result.stderr):
216
+ raise AgentServiceError("Unable to unload Project Agent LaunchAgent.")
217
+ locations.plist_path.unlink(missing_ok=True)
218
+ # State is installation-owned and transient. Config/identity (and any future
219
+ # credential reference) are intentionally preserved for explicit reset only.
220
+ if locations.state_dir.exists():
221
+ shutil.rmtree(locations.state_dir)
222
+
223
+
224
+ def status(paths: AgentPaths | None = None, *, runner: Runner | None = None) -> dict[str, str]:
225
+ locations = paths or default_paths()
226
+ if not locations.plist_path.exists():
227
+ return {"state": "not-installed"}
228
+ try:
229
+ AgentConfiguration.load(locations.config_path)
230
+ except AgentServiceError:
231
+ return {"state": "misconfigured"}
232
+ result = _launchctl(("print", f"{_domain()}/{LABEL}"), runner)
233
+ return {"state": "running" if result.returncode == 0 else "stopped"}
234
+
235
+
236
+ def _not_loaded(stderr: str) -> bool:
237
+ return any(marker in (stderr or "").lower() for marker in ("could not find service", "no such process", "not found"))
238
+
239
+
240
+ def run(config_path: Path, *, poll_seconds: float = 30.0) -> int:
241
+ """Long-running, intentionally unpaired process for launchd supervision."""
242
+ AgentConfiguration.load(config_path)
243
+ while True:
244
+ time.sleep(poll_seconds)
@@ -0,0 +1,126 @@
1
+ """Durable logical project topology and Agent checkout attachment authority.
2
+
3
+ Repository declarations are the sole source of project and repository identity.
4
+ This module never receives a checkout path and therefore cannot turn local
5
+ inventory into portable topology.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sqlite3
11
+ from datetime import datetime, timedelta, timezone
12
+ from typing import Any
13
+
14
+ from .repository_attachment import RepositoryAttachmentError, parse_repository_attachment
15
+
16
+
17
+ class TopologyRegistrationError(ValueError):
18
+ """Stable, fail-closed attachment-registration error."""
19
+
20
+
21
+ def _error(code: str) -> None:
22
+ raise TopologyRegistrationError(code)
23
+
24
+
25
+ def _now() -> str:
26
+ return datetime.now(timezone.utc).isoformat()
27
+
28
+
29
+ def install_schema(connection: sqlite3.Connection) -> None:
30
+ connection.execute("""CREATE TABLE IF NOT EXISTS ep_repository_registrations (
31
+ repository_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
32
+ authority_repository_id TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('authority','child')),
33
+ attachment_contract TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)""")
34
+ connection.execute("CREATE INDEX IF NOT EXISTS ep_repository_registrations_project_lookup ON ep_repository_registrations(project_id,repository_id)")
35
+ connection.execute("""CREATE TABLE IF NOT EXISTS ep_agent_repository_attachments (
36
+ agent_id TEXT NOT NULL REFERENCES ep_agent_registrations(agent_id),
37
+ repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
38
+ availability TEXT NOT NULL CHECK(availability IN ('AVAILABLE','UNAVAILABLE')),
39
+ checkout_evidence TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
40
+ PRIMARY KEY(agent_id,repository_id))""")
41
+ connection.execute("CREATE INDEX IF NOT EXISTS ep_agent_repository_attachments_repository_lookup ON ep_agent_repository_attachments(repository_id,agent_id)")
42
+
43
+
44
+ def register_attachment(connection: sqlite3.Connection, *, agent_id: str, declaration: object, availability: object) -> dict[str, str]:
45
+ """Revalidate, reconcile, and atomically persist one Agent attachment."""
46
+ if availability != "AVAILABLE":
47
+ _error("INVALID_ATTACHMENT_AVAILABILITY")
48
+ try:
49
+ attachment = parse_repository_attachment(declaration)
50
+ except RepositoryAttachmentError as error:
51
+ _error("MALFORMED_REPOSITORY_DECLARATION")
52
+ raise AssertionError from error
53
+ surface = attachment.agent_read_surface()
54
+ canonical = json.dumps(surface, sort_keys=True, separators=(",", ":"))
55
+ now = _now()
56
+ existing_project = connection.execute("SELECT attachment_contract FROM ep_project_registrations WHERE project_id=?", (attachment.project_id,)).fetchone()
57
+ if existing_project is not None:
58
+ try:
59
+ old = json.loads(str(existing_project[0]))
60
+ if old.get("authority_repository_id") != attachment.authority_repository_id:
61
+ _error("PROJECT_DECLARATION_CONFLICT")
62
+ except (json.JSONDecodeError, AttributeError):
63
+ _error("PROJECT_DECLARATION_CONFLICT")
64
+ existing_repository = connection.execute("SELECT project_id,authority_repository_id,role,attachment_contract FROM ep_repository_registrations WHERE repository_id=?", (attachment.repository_id,)).fetchone()
65
+ if existing_repository is not None:
66
+ if tuple(existing_repository[:3]) != (attachment.project_id, attachment.authority_repository_id, attachment.repository_role):
67
+ _error("REPOSITORY_IDENTITY_CONFLICT")
68
+ if str(existing_repository[3]) != canonical:
69
+ _error("REPOSITORY_DECLARATION_CONFLICT")
70
+ authority = connection.execute("SELECT project_id,role FROM ep_repository_registrations WHERE repository_id=?", (attachment.authority_repository_id,)).fetchone()
71
+ if authority is not None and tuple(authority) != (attachment.project_id, "authority"):
72
+ _error("AUTHORITY_REPOSITORY_CONFLICT")
73
+ if attachment.repository_role == "authority" and attachment.repository_id != attachment.authority_repository_id:
74
+ _error("AUTHORITY_REPOSITORY_MISMATCH")
75
+ project_contract = json.dumps({"schema_version": surface["schema_version"], "authority_repository_id": attachment.authority_repository_id}, sort_keys=True, separators=(",", ":"))
76
+ connection.execute("""INSERT INTO ep_project_registrations(project_id,attachment_contract,status,created_at,updated_at)
77
+ VALUES(?,?, 'ACTIVE', ?, ?) ON CONFLICT(project_id) DO UPDATE SET updated_at=excluded.updated_at""", (attachment.project_id, project_contract, now, now))
78
+ connection.execute("""INSERT INTO ep_repository_registrations(repository_id,project_id,authority_repository_id,role,attachment_contract,created_at,updated_at)
79
+ VALUES(?,?,?,?,?,?,?) ON CONFLICT(repository_id) DO UPDATE SET updated_at=excluded.updated_at""", (attachment.repository_id, attachment.project_id, attachment.authority_repository_id, attachment.repository_role, canonical, now, now))
80
+ connection.execute("""INSERT INTO ep_agent_repository_attachments(agent_id,repository_id,availability,checkout_evidence,created_at,updated_at)
81
+ VALUES(?,?, 'AVAILABLE','declared-checkout',?,?) ON CONFLICT(agent_id,repository_id) DO UPDATE SET availability='AVAILABLE',checkout_evidence='declared-checkout',updated_at=excluded.updated_at""", (agent_id, attachment.repository_id, now, now))
82
+ return {"project_id": attachment.project_id, "repository_id": attachment.repository_id, "availability": "AVAILABLE", "result": "REGISTERED"}
83
+
84
+
85
+ def register_server_local_topology(connection: sqlite3.Connection, *, declaration: object) -> dict[str, str]:
86
+ """Register explicit logical Phase-P topology without an Agent attachment."""
87
+ try:
88
+ attachment = parse_repository_attachment(declaration)
89
+ except RepositoryAttachmentError as error:
90
+ _error("MALFORMED_REPOSITORY_DECLARATION")
91
+ raise AssertionError from error
92
+ surface = attachment.agent_read_surface()
93
+ canonical = json.dumps(surface, sort_keys=True, separators=(",", ":"))
94
+ now = _now()
95
+ existing = connection.execute("SELECT project_id,authority_repository_id,role,attachment_contract FROM ep_repository_registrations WHERE repository_id=?", (attachment.repository_id,)).fetchone()
96
+ if existing is not None and tuple(existing[:3]) != (attachment.project_id, attachment.authority_repository_id, attachment.repository_role):
97
+ _error("REPOSITORY_IDENTITY_CONFLICT")
98
+ if existing is not None and str(existing[3]) != canonical:
99
+ _error("REPOSITORY_DECLARATION_CONFLICT")
100
+ if attachment.repository_role == "authority" and attachment.repository_id != attachment.authority_repository_id:
101
+ _error("AUTHORITY_REPOSITORY_MISMATCH")
102
+ contract = json.dumps({"schema_version": surface["schema_version"], "authority_repository_id": attachment.authority_repository_id}, sort_keys=True, separators=(",", ":"))
103
+ connection.execute("INSERT INTO ep_project_registrations(project_id,attachment_contract,status,created_at,updated_at) VALUES(?,?, 'ACTIVE', ?, ?) ON CONFLICT(project_id) DO UPDATE SET updated_at=excluded.updated_at", (attachment.project_id, contract, now, now))
104
+ connection.execute("INSERT INTO ep_repository_registrations(repository_id,project_id,authority_repository_id,role,attachment_contract,created_at,updated_at) VALUES(?,?,?,?,?,?,?) ON CONFLICT(repository_id) DO UPDATE SET updated_at=excluded.updated_at", (attachment.repository_id, attachment.project_id, attachment.authority_repository_id, attachment.repository_role, canonical, now, now))
105
+ return {"project_id": attachment.project_id, "repository_id": attachment.repository_id, "result": "REGISTERED"}
106
+
107
+
108
+ def topology(connection: sqlite3.Connection) -> dict[str, object]:
109
+ """Bounded, secret-free registration diagnostic."""
110
+ cutoff = (datetime.now(timezone.utc) - timedelta(seconds=90)).isoformat()
111
+ projects: dict[str, dict[str, Any]] = {}
112
+ for project_id, authority_id in connection.execute("SELECT project_id,json_extract(attachment_contract, '$.authority_repository_id') FROM ep_project_registrations ORDER BY project_id"):
113
+ projects[str(project_id)] = {"project_id": str(project_id), "authority_repository_id": str(authority_id), "repositories": []}
114
+ rows = connection.execute("""SELECT r.project_id,r.repository_id,r.role,a.agent_id,a.availability,a.updated_at,g.state,g.last_seen_at
115
+ FROM ep_repository_registrations r LEFT JOIN ep_agent_repository_attachments a ON a.repository_id=r.repository_id
116
+ LEFT JOIN ep_agent_registrations g ON g.agent_id=a.agent_id ORDER BY r.project_id,r.repository_id,a.agent_id""")
117
+ repository_entries: dict[tuple[str, str], dict[str, Any]] = {}
118
+ for project_id, repository_id, role, agent_id, availability, updated_at, state, last_seen in rows:
119
+ key = (str(project_id), str(repository_id))
120
+ entry = repository_entries.setdefault(key, {"repository_id": key[1], "role": str(role), "attachments": []})
121
+ if agent_id is not None:
122
+ available = availability == "AVAILABLE" and state != "REVOKED" and last_seen is not None and str(last_seen) >= cutoff
123
+ entry["attachments"].append({"agent_id": str(agent_id), "availability": "AVAILABLE" if available else "UNAVAILABLE", "last_seen_at": str(updated_at)})
124
+ for (project_id, _), repository in repository_entries.items():
125
+ projects[project_id]["repositories"].append(repository)
126
+ return {"projects": list(projects.values())}