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,209 @@
1
+ """Historical Local Consumer API contract fixture; not an EP service.
2
+
3
+ The current product's supported submission ingress is the Server HTTP JSON
4
+ endpoint, installed CLI and Server-owned File Inbox. This module retains the
5
+ small read-only contract fixture needed by provenance and regression tests; it
6
+ does not provide an install, LaunchAgent, lifecycle, repair or health surface.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from .contracts.ep_consumer import (
16
+ ContractError,
17
+ ErrorCode,
18
+ ErrorEnvelope,
19
+ RequestEnvelope,
20
+ ResponseEnvelope,
21
+ )
22
+ from .ep_consumer_credentials import CredentialAuthority
23
+ from .storage import open_storage
24
+
25
+ # These labels are historical identities only. They are deliberately not
26
+ # component identifiers, lifecycle targets or installation defaults.
27
+ HISTORICAL_LEGACY_LABEL = "com.djconnect.engineering-local-api"
28
+ HISTORICAL_NEUTRAL_LABEL = "com.engineeringplatform.local-api"
29
+ LOOPBACK_ADDRESS = "127.0.0.1"
30
+ DEFAULT_PORT = 8766
31
+ MAX_BODY_BYTES = 8192
32
+ REQUEST_TIMEOUT_SECONDS = 15
33
+
34
+
35
+ def valid_port(value: object) -> int:
36
+ if not isinstance(value, int) or isinstance(value, bool) or not 1024 <= value <= 65535:
37
+ raise ValueError("Local Consumer API port is invalid.")
38
+ return value
39
+
40
+
41
+ def _error(
42
+ handler: BaseHTTPRequestHandler,
43
+ status: int,
44
+ request_id: str | None = None,
45
+ code: str = ErrorCode.MALFORMED_REQUEST,
46
+ ) -> None:
47
+ body = ErrorEnvelope(request_id, code).serialize().encode("utf-8")
48
+ handler.send_response(status)
49
+ handler.send_header("Content-Type", "application/json; charset=utf-8")
50
+ handler.send_header("Content-Length", str(len(body)))
51
+ handler.end_headers()
52
+ handler.wfile.write(body)
53
+
54
+
55
+ def _contract_error(handler: BaseHTTPRequestHandler, status: int, error: ContractError) -> None:
56
+ body = error.to_error_envelope().serialize().encode("utf-8")
57
+ handler.send_response(status)
58
+ handler.send_header("Content-Type", "application/json; charset=utf-8")
59
+ handler.send_header("Content-Length", str(len(body)))
60
+ handler.end_headers()
61
+ handler.wfile.write(body)
62
+
63
+
64
+ class LocalApiServer(ThreadingHTTPServer):
65
+ allow_reuse_address = True
66
+ request_queue_size = 16
67
+
68
+ def __init__(
69
+ self, root: Path, port: int = DEFAULT_PORT, authority: CredentialAuthority | None = None
70
+ ) -> None:
71
+ self.root, self.authority = root, authority or CredentialAuthority(root)
72
+ # Ephemeral port zero is test-only; production CLI/configuration still
73
+ # goes through the bounded valid_port contract.
74
+ if port != 0:
75
+ port = valid_port(port)
76
+ super().__init__((LOOPBACK_ADDRESS, port), LocalApiHandler)
77
+ self.timeout = REQUEST_TIMEOUT_SECONDS
78
+
79
+ def ready(self) -> bool:
80
+ return readiness(self.root, self.authority)
81
+
82
+ def handle_error(self, request: object, client_address: object) -> None:
83
+ """Avoid socket-level tracebacks and never surface request data."""
84
+
85
+ del request, client_address
86
+
87
+
88
+ def readiness(root: Path, authority: CredentialAuthority | None = None) -> bool:
89
+ """Return readiness without creating a second listener."""
90
+
91
+ try:
92
+ connection = open_storage(root)
93
+ connection.close()
94
+ except Exception:
95
+ return False
96
+ return (authority or CredentialAuthority(root)).ready()
97
+
98
+
99
+ class LocalApiHandler(BaseHTTPRequestHandler):
100
+ server: LocalApiServer
101
+ protocol_version = "HTTP/1.1"
102
+
103
+ def log_message(self, *_: object) -> None:
104
+ pass
105
+
106
+ def setup(self) -> None:
107
+ super().setup()
108
+ self.connection.settimeout(REQUEST_TIMEOUT_SECONDS)
109
+
110
+ def _json(self, status: int, payload: dict[str, object]) -> None:
111
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
112
+ self.send_response(status)
113
+ self.send_header("Content-Type", "application/json; charset=utf-8")
114
+ self.send_header("Content-Length", str(len(body)))
115
+ self.end_headers()
116
+ self.wfile.write(body)
117
+
118
+ def do_GET(self) -> None:
119
+ if self.path != "/health":
120
+ _error(self, 404)
121
+ return
122
+ ready = self.server.ready()
123
+ self._json(
124
+ 200 if ready else 503,
125
+ {"health": "ok" if ready else "not_ready", "healthy": ready, "contract_version": "1.0"},
126
+ )
127
+
128
+ def do_POST(self) -> None:
129
+ if self.path != "/v1/capabilities":
130
+ _error(self, 404)
131
+ return
132
+ if self.headers.get_content_type() != "application/json":
133
+ _error(self, 415)
134
+ return
135
+ try:
136
+ size = int(self.headers.get("Content-Length", "-1"))
137
+ except ValueError:
138
+ size = -1
139
+ if not 0 <= size <= MAX_BODY_BYTES:
140
+ _error(self, 413)
141
+ return
142
+ try:
143
+ envelope = RequestEnvelope.parse(json.loads(self.rfile.read(size)))
144
+ except ContractError as error:
145
+ _contract_error(self, 400, error)
146
+ return
147
+ except json.JSONDecodeError:
148
+ _error(self, 400)
149
+ return
150
+ if envelope.payload:
151
+ _error(self, 400, envelope.request_id, ErrorCode.MALFORMED_REQUEST)
152
+ return
153
+ if not self.server.ready():
154
+ _error(self, 503, envelope.request_id, ErrorCode.SERVICE_NOT_READY)
155
+ return
156
+ header = self.headers.get("Authorization")
157
+ if not isinstance(header, str) or not header.startswith("Bearer ") or not header[7:]:
158
+ _error(self, 401, envelope.request_id, ErrorCode.UNAUTHENTICATED)
159
+ return
160
+ scope = self.server.authority.authenticate(header[7:])
161
+ if scope is None:
162
+ _error(self, 401, envelope.request_id, ErrorCode.UNAUTHENTICATED)
163
+ return
164
+ if not self.server.authority.authorized(scope):
165
+ _error(self, 403, envelope.request_id, ErrorCode.PROJECT_NOT_AUTHORIZED)
166
+ return
167
+ if (
168
+ scope.consumer_id != envelope.consumer.consumer_id
169
+ or scope.project_id != envelope.project_id
170
+ ):
171
+ _error(self, 403, envelope.request_id, ErrorCode.PROJECT_NOT_AUTHORIZED)
172
+ return
173
+ response = ResponseEnvelope(
174
+ envelope.request_id,
175
+ {
176
+ "contract_version": "1.0",
177
+ "project_id": envelope.project_id,
178
+ "capabilities": ["contract.foundation"],
179
+ "read_only": True,
180
+ },
181
+ )
182
+ self._json(200, response.to_dict())
183
+
184
+ def do_PUT(self) -> None:
185
+ _error(self, 405)
186
+
187
+ def do_DELETE(self) -> None:
188
+ _error(self, 405)
189
+
190
+ def do_PATCH(self) -> None:
191
+ _error(self, 405)
192
+
193
+ def do_HEAD(self) -> None:
194
+ _error(self, 405)
195
+
196
+ def do_OPTIONS(self) -> None:
197
+ _error(self, 405)
198
+
199
+
200
+ def retirement_status() -> dict[str, object]:
201
+ """Expose the bounded historical identity classification for tooling."""
202
+
203
+ return {
204
+ "state": "RETIRED",
205
+ "historical_labels": (HISTORICAL_LEGACY_LABEL, HISTORICAL_NEUTRAL_LABEL),
206
+ "installable": False,
207
+ "lifecycle_owned": False,
208
+ "supported_submission_ingress": False,
209
+ }
@@ -0,0 +1,51 @@
1
+ """Consumer-side macOS Keychain credential store; never an EP auth authority."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ SERVICE = "Engineering Platform Local Consumer API"
8
+
9
+
10
+ class KeychainError(RuntimeError):
11
+ """Safe failure category; command diagnostics can contain secret material."""
12
+
13
+
14
+ def _account(consumer_id: str, project_id: str) -> str:
15
+ return f"{consumer_id}:{project_id}"
16
+
17
+
18
+ class MacOSKeychainCredentialStore:
19
+ """Bounded current-user Keychain adapter with no plaintext fallback."""
20
+
21
+ def __init__(self, executable: str = "security", service: str = SERVICE) -> None:
22
+ self.executable, self.service = executable, service
23
+
24
+ def _run(self, arguments: list[str], *, input_value: str | None = None) -> subprocess.CompletedProcess[str]:
25
+ try:
26
+ return subprocess.run([self.executable, *arguments], input=input_value, text=True, capture_output=True, check=False)
27
+ except OSError as error:
28
+ raise KeychainError("macOS Keychain is unavailable.") from error
29
+
30
+ def put_credential(self, consumer_id: str, project_id: str, token: str) -> None:
31
+ result = self._run(["add-generic-password", "-U", "-s", self.service, "-a", _account(consumer_id, project_id), "-w", token])
32
+ if result.returncode:
33
+ raise KeychainError("macOS Keychain could not store the credential.")
34
+
35
+ def get_credential(self, consumer_id: str, project_id: str) -> str:
36
+ result = self._run(["find-generic-password", "-w", "-s", self.service, "-a", _account(consumer_id, project_id)])
37
+ if result.returncode or not result.stdout:
38
+ raise KeychainError("macOS Keychain credential is missing or unavailable.")
39
+ return result.stdout.rstrip("\n")
40
+
41
+ def delete_credential(self, consumer_id: str, project_id: str) -> None:
42
+ result = self._run(["delete-generic-password", "-s", self.service, "-a", _account(consumer_id, project_id)])
43
+ if result.returncode:
44
+ raise KeychainError("macOS Keychain credential could not be deleted.")
45
+
46
+ def credential_present(self, consumer_id: str, project_id: str) -> bool:
47
+ try:
48
+ self.get_credential(consumer_id, project_id)
49
+ except KeychainError:
50
+ return False
51
+ return True
@@ -0,0 +1,138 @@
1
+ """Private CENTRAL mappings from logical repositories to local checkout roots.
2
+
3
+ This is intentionally an operator-only Phase-P bridge. It does not discover
4
+ checkouts, consult Agent storage, or grant execution authority.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ import sqlite3
12
+
13
+ from .repository_attachment import RepositoryAttachmentError, load_repository_attachment
14
+
15
+
16
+ class LocalRepositoryBindingError(ValueError):
17
+ """A stable, redacted failure for local-binding lifecycle operations."""
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class LocalRepositoryBinding:
22
+ project_id: str
23
+ repository_id: str
24
+ local_root: Path
25
+ state: str
26
+ created_at: str
27
+ updated_at: str
28
+
29
+
30
+ def _fail(code: str) -> None:
31
+ raise LocalRepositoryBindingError(code)
32
+
33
+
34
+ def _now() -> str:
35
+ return datetime.now(timezone.utc).isoformat()
36
+
37
+
38
+ def install_schema(connection: sqlite3.Connection) -> None:
39
+ connection.execute("""CREATE TABLE IF NOT EXISTS ep_local_repository_bindings (
40
+ project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id),
41
+ repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id),
42
+ local_root TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('BOUND','UNBOUND')),
43
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
44
+ PRIMARY KEY(project_id,repository_id))""")
45
+ connection.execute("CREATE INDEX IF NOT EXISTS ep_local_repository_bindings_repository_lookup ON ep_local_repository_bindings(repository_id,state)")
46
+
47
+
48
+ def _validated_root(local_root: object, *, data_root: Path | None = None) -> Path:
49
+ if not isinstance(local_root, (str, Path)):
50
+ _fail("LOCAL_ROOT_INVALID")
51
+ candidate = Path(local_root).expanduser()
52
+ if not candidate.is_absolute():
53
+ _fail("LOCAL_ROOT_NOT_ABSOLUTE")
54
+ try:
55
+ root = candidate.resolve(strict=True)
56
+ except OSError:
57
+ _fail("LOCAL_ROOT_UNAVAILABLE")
58
+ if not root.is_dir():
59
+ _fail("LOCAL_ROOT_NOT_DIRECTORY")
60
+ # Never allow a filesystem anchor, home directory, CENTRAL data root, or
61
+ # the installed package/runtime tree to be accidentally treated as a repo.
62
+ forbidden = {root.anchor, str(Path.home().resolve())}
63
+ if str(root) in forbidden:
64
+ _fail("LOCAL_ROOT_UNSAFE")
65
+ runtime_root = Path(__file__).resolve().parents[1]
66
+ protected = [runtime_root]
67
+ if data_root is not None:
68
+ try:
69
+ protected.append(data_root.resolve())
70
+ except OSError:
71
+ _fail("LOCAL_ROOT_UNAVAILABLE")
72
+ if any(root == protected_root or root.is_relative_to(protected_root) for protected_root in protected):
73
+ _fail("LOCAL_ROOT_UNSAFE")
74
+ return root
75
+
76
+
77
+ def _validate_topology(connection: sqlite3.Connection, project_id: str, repository_id: str) -> None:
78
+ project = connection.execute("SELECT 1 FROM ep_project_registrations WHERE project_id=?", (project_id,)).fetchone()
79
+ if project is None:
80
+ _fail("UNKNOWN_PROJECT")
81
+ repository = connection.execute("SELECT project_id FROM ep_repository_registrations WHERE repository_id=?", (repository_id,)).fetchone()
82
+ if repository is None:
83
+ _fail("UNKNOWN_REPOSITORY")
84
+ if str(repository[0]) != project_id:
85
+ _fail("PROJECT_REPOSITORY_MISMATCH")
86
+
87
+
88
+ def _validate_declaration(root: Path, project_id: str, repository_id: str) -> None:
89
+ try:
90
+ declaration = load_repository_attachment(root)
91
+ except RepositoryAttachmentError:
92
+ _fail("REPOSITORY_DECLARATION_INVALID")
93
+ if declaration.project_id != project_id or declaration.repository_id != repository_id:
94
+ _fail("REPOSITORY_DECLARATION_MISMATCH")
95
+
96
+
97
+ def bind_local_repository(
98
+ connection: sqlite3.Connection, *, project_id: str, repository_id: str,
99
+ local_root: object, data_root: Path | None = None, rebind: bool = False,
100
+ ) -> LocalRepositoryBinding:
101
+ """Create or deliberately replace the one active path for a repository."""
102
+ _validate_topology(connection, project_id, repository_id)
103
+ root = _validated_root(local_root, data_root=data_root)
104
+ _validate_declaration(root, project_id, repository_id)
105
+ existing = connection.execute("SELECT local_root,state,created_at FROM ep_local_repository_bindings WHERE project_id=? AND repository_id=?", (project_id, repository_id)).fetchone()
106
+ if existing is not None and str(existing[0]) != str(root) and not rebind:
107
+ _fail("LOCAL_BINDING_EXISTS_REBIND_REQUIRED")
108
+ now = _now()
109
+ if existing is None:
110
+ connection.execute("INSERT INTO ep_local_repository_bindings(project_id,repository_id,local_root,state,created_at,updated_at) VALUES(?,?,?,'BOUND',?,?)", (project_id, repository_id, str(root), now, now))
111
+ created_at = now
112
+ else:
113
+ connection.execute("UPDATE ep_local_repository_bindings SET local_root=?,state='BOUND',updated_at=? WHERE project_id=? AND repository_id=?", (str(root), now, project_id, repository_id))
114
+ created_at = str(existing[2])
115
+ return LocalRepositoryBinding(project_id, repository_id, root, "BOUND", created_at, now)
116
+
117
+
118
+ def resolve_local_repository_binding(connection: sqlite3.Connection, *, project_id: str, repository_id: str, data_root: Path | None = None) -> LocalRepositoryBinding:
119
+ """Return the current validated root, or fail closed without CWD fallback."""
120
+ _validate_topology(connection, project_id, repository_id)
121
+ row = connection.execute("SELECT local_root,state,created_at,updated_at FROM ep_local_repository_bindings WHERE project_id=? AND repository_id=?", (project_id, repository_id)).fetchone()
122
+ if row is None or str(row[1]) != "BOUND":
123
+ _fail("LOCAL_BINDING_UNBOUND")
124
+ root = _validated_root(str(row[0]), data_root=data_root)
125
+ _validate_declaration(root, project_id, repository_id)
126
+ return LocalRepositoryBinding(project_id, repository_id, root, "BOUND", str(row[2]), str(row[3]))
127
+
128
+
129
+ def unbind_local_repository(connection: sqlite3.Connection, *, project_id: str, repository_id: str) -> None:
130
+ """Disable the mapping while preserving historical binding evidence."""
131
+ _validate_topology(connection, project_id, repository_id)
132
+ now = _now()
133
+ connection.execute("UPDATE ep_local_repository_bindings SET state='UNBOUND',updated_at=? WHERE project_id=? AND repository_id=?", (now, project_id, repository_id))
134
+
135
+
136
+ # Phase-P's exact internal boundary. The alias keeps call sites free of CLI
137
+ # wording and makes clear that lookup itself supplies no execution eligibility.
138
+ resolve_execution_repository = resolve_local_repository_binding