engineering-platform 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of engineering-platform might be problematic. Click here for more details.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Public, provider-neutral Engineering Platform API.
|
|
2
|
+
|
|
3
|
+
This module is the supported boundary for workspace-aware platform consumers.
|
|
4
|
+
Provider implementations remain private and existing command wrappers remain
|
|
5
|
+
compatible.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import json
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from .providers import engineering_platform_codex_cli_prefix, registry
|
|
16
|
+
from .resources import package_text
|
|
17
|
+
from .platform_version import CURRENT_PLATFORM_VERSION
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlatformConfigurationError(ValueError):
|
|
21
|
+
"""Raised for invalid or unsupported platform configuration."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# launchd deliberately starts with a minimal environment. Preserve the
|
|
25
|
+
# resolved launcher rather than depending on a later child process finding a
|
|
26
|
+
# different PATH. The value is local host configuration, not product data.
|
|
27
|
+
RUNTIME_EXECUTABLE_ENVIRONMENT = "ENGINEERING_PLATFORM_CODEX_EXECUTABLE"
|
|
28
|
+
RUNTIME_PATH_FALLBACK = (
|
|
29
|
+
"/opt/homebrew/bin",
|
|
30
|
+
"/usr/local/bin",
|
|
31
|
+
"/usr/bin",
|
|
32
|
+
"/bin",
|
|
33
|
+
"/usr/sbin",
|
|
34
|
+
"/sbin",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def shared_workspace_store(root: Path) -> Path:
|
|
39
|
+
"""Resolve the private Engineering store shared by every Git worktree.
|
|
40
|
+
|
|
41
|
+
A linked worktree has a ``.git`` *file* that points at its private Git
|
|
42
|
+
directory. Git's ``commondir`` file then identifies the repository-wide
|
|
43
|
+
Git directory. Keeping local Engineering evidence immediately below that
|
|
44
|
+
common directory makes it independent of the currently checked-out
|
|
45
|
+
worktree while remaining private and repository-scoped.
|
|
46
|
+
|
|
47
|
+
Non-Git callers deliberately retain the old local layout. This keeps
|
|
48
|
+
isolated test fixtures and explicit non-repository tooling self-contained.
|
|
49
|
+
"""
|
|
50
|
+
root = root.resolve()
|
|
51
|
+
git_marker = root / ".git"
|
|
52
|
+
if git_marker.is_dir():
|
|
53
|
+
common = git_marker.resolve()
|
|
54
|
+
elif git_marker.is_file():
|
|
55
|
+
try:
|
|
56
|
+
line = git_marker.read_text(encoding="utf-8").strip()
|
|
57
|
+
prefix = "gitdir: "
|
|
58
|
+
if not line.startswith(prefix):
|
|
59
|
+
return root / ".engineering"
|
|
60
|
+
git_dir = Path(line[len(prefix):].strip())
|
|
61
|
+
if not git_dir.is_absolute():
|
|
62
|
+
git_dir = git_marker.parent / git_dir
|
|
63
|
+
common_marker = git_dir.resolve() / "commondir"
|
|
64
|
+
if not common_marker.is_file():
|
|
65
|
+
return root / ".engineering"
|
|
66
|
+
common_path = common_marker.read_text(encoding="utf-8").strip()
|
|
67
|
+
common = Path(common_path)
|
|
68
|
+
if not common.is_absolute():
|
|
69
|
+
common = common_marker.parent / common
|
|
70
|
+
common = common.resolve()
|
|
71
|
+
except OSError:
|
|
72
|
+
return root / ".engineering"
|
|
73
|
+
else:
|
|
74
|
+
return root / ".engineering"
|
|
75
|
+
return common / "engineering-platform"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class PlatformIdentity:
|
|
80
|
+
id: str
|
|
81
|
+
name: str
|
|
82
|
+
version: str
|
|
83
|
+
generation: int
|
|
84
|
+
documentation_namespace: str
|
|
85
|
+
capability_registry_version: int
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class WorkspaceIdentity:
|
|
90
|
+
id: str
|
|
91
|
+
name: str
|
|
92
|
+
repository_provider: str
|
|
93
|
+
repository_owner: str
|
|
94
|
+
repository_name: str
|
|
95
|
+
default_branch: str
|
|
96
|
+
dashboard_title: str
|
|
97
|
+
provisioning_root: str | None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class AllowedWorkspaceRoot:
|
|
102
|
+
"""A trusted directory which may contain engineering repositories."""
|
|
103
|
+
|
|
104
|
+
path: str
|
|
105
|
+
repository_scope: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class RepositoryAuthorizationPolicy:
|
|
110
|
+
"""Fail-closed repository admission policy owned by host configuration."""
|
|
111
|
+
|
|
112
|
+
allowed_roots: tuple[AllowedWorkspaceRoot, ...]
|
|
113
|
+
allowed_repositories: tuple[str, ...]
|
|
114
|
+
denied_repositories: tuple[str, ...]
|
|
115
|
+
symlink_policy: str
|
|
116
|
+
case_sensitivity: str
|
|
117
|
+
legacy: bool = False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class RepositoryAuthorization:
|
|
122
|
+
authorized: bool
|
|
123
|
+
canonical_target: str | None
|
|
124
|
+
matched: str | None
|
|
125
|
+
scope: str | None
|
|
126
|
+
reason: str
|
|
127
|
+
recovery: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class RuntimePromptTransport:
|
|
132
|
+
"""Provider-neutral runtime-prompt transport; paths stay resolver-owned."""
|
|
133
|
+
|
|
134
|
+
provider: str
|
|
135
|
+
inbox: Path
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class ExecutionHostIdentity:
|
|
140
|
+
name: str
|
|
141
|
+
version: str
|
|
142
|
+
runtime: str
|
|
143
|
+
runtime_prompt_transport: str
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ExecutionHostConfigurationResolver:
|
|
147
|
+
"""The sole host-specific configuration and location resolution boundary."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, root: Path, configuration: "PlatformConfiguration") -> None:
|
|
150
|
+
self._root = root.resolve()
|
|
151
|
+
self._configuration = configuration
|
|
152
|
+
|
|
153
|
+
def resolve_runtime_prompt_transport(self) -> RuntimePromptTransport:
|
|
154
|
+
provider = self._configuration.providers["remote_submission"]
|
|
155
|
+
if provider != "icloud_inbox":
|
|
156
|
+
raise PlatformConfigurationError("Configured Runtime Prompt transport is unsupported.")
|
|
157
|
+
# Root-local dashboard settings and environment overrides are retired
|
|
158
|
+
# from the supported runtime. File ingress is Server-owned; this
|
|
159
|
+
# legacy resolver has one deterministic historical fallback only.
|
|
160
|
+
inbox_root = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Engineering Platform"
|
|
161
|
+
return RuntimePromptTransport(provider, inbox_root / "Inbox")
|
|
162
|
+
|
|
163
|
+
def resolve_workspace_store(self) -> Path:
|
|
164
|
+
return shared_workspace_store(self._root)
|
|
165
|
+
|
|
166
|
+
def resolve_status_store(self) -> Path:
|
|
167
|
+
return self.resolve_workspace_store() / "status"
|
|
168
|
+
|
|
169
|
+
def resolve_report_store(self) -> Path:
|
|
170
|
+
return self.resolve_workspace_store() / "reports"
|
|
171
|
+
|
|
172
|
+
def resolve_log_store(self) -> Path:
|
|
173
|
+
return self.resolve_workspace_store() / "logs"
|
|
174
|
+
|
|
175
|
+
def resolve_telemetry_store(self) -> Path:
|
|
176
|
+
return self.resolve_workspace_store() / "engineering.db"
|
|
177
|
+
|
|
178
|
+
def resolve_runtime(self) -> Path:
|
|
179
|
+
if self._configuration.providers["runtime"] != "codex_cli":
|
|
180
|
+
raise PlatformConfigurationError("Configured Execution Host runtime is unsupported.")
|
|
181
|
+
executable = engineering_platform_codex_cli_prefix() / "bin" / "codex"
|
|
182
|
+
if not executable.is_file() or not os.access(executable, os.X_OK):
|
|
183
|
+
raise PlatformConfigurationError("Engineering Platform managed Codex CLI is unavailable.")
|
|
184
|
+
return executable
|
|
185
|
+
|
|
186
|
+
def runtime_environment(self) -> dict[str, str]:
|
|
187
|
+
"""Return a child-safe environment pinned to the admitted CLI launcher."""
|
|
188
|
+
executable = self.resolve_runtime()
|
|
189
|
+
entries = [str(executable.parent), *RUNTIME_PATH_FALLBACK]
|
|
190
|
+
entries.extend(os.environ.get("PATH", "").split(":"))
|
|
191
|
+
return {
|
|
192
|
+
RUNTIME_EXECUTABLE_ENVIRONMENT: str(executable),
|
|
193
|
+
"PATH": ":".join(dict.fromkeys(entry for entry in entries if entry)),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
def resolve_execution_host_identity(self) -> ExecutionHostIdentity:
|
|
197
|
+
return ExecutionHostIdentity(
|
|
198
|
+
self._configuration.platform.name,
|
|
199
|
+
self._configuration.platform.version,
|
|
200
|
+
self._configuration.providers["runtime"],
|
|
201
|
+
self._configuration.providers["remote_submission"],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class PlatformConfiguration:
|
|
207
|
+
schema_version: int
|
|
208
|
+
platform: PlatformIdentity
|
|
209
|
+
workspace: WorkspaceIdentity
|
|
210
|
+
providers: dict[str, str]
|
|
211
|
+
repository_authorization: RepositoryAuthorizationPolicy
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def load(cls, root: Path) -> "PlatformConfiguration":
|
|
215
|
+
try:
|
|
216
|
+
raw = json.loads(package_text("ENGINEERING_PLATFORM_CONFIG.json"))
|
|
217
|
+
local = root / ".engineering" / "engineering-platform.local.json"
|
|
218
|
+
if local.is_file():
|
|
219
|
+
override = json.loads(local.read_text(encoding="utf-8"))
|
|
220
|
+
if set(override) - {"workspace"}:
|
|
221
|
+
raise PlatformConfigurationError("Local Engineering Platform configuration is incompatible.")
|
|
222
|
+
raw = _merge(raw, override)
|
|
223
|
+
platform, workspace = raw["platform"], raw["workspace"]
|
|
224
|
+
repository, branding = workspace["repository"], workspace["branding"]
|
|
225
|
+
providers = raw["providers"]
|
|
226
|
+
except (OSError, KeyError, TypeError, json.JSONDecodeError) as error:
|
|
227
|
+
raise PlatformConfigurationError("Engineering Platform configuration is invalid.") from error
|
|
228
|
+
expected = {"runtime", "repository", "service_manager", "remote_submission", "private_remote_access", "dashboard"}
|
|
229
|
+
if set(raw) != {"schema_version", "platform", "workspace", "providers"} or raw.get("schema_version") not in {1, 2} or set(providers) != expected or not all(isinstance(v, str) and v for v in providers.values()):
|
|
230
|
+
raise PlatformConfigurationError("Engineering Platform configuration is incompatible.")
|
|
231
|
+
provisioning_root = workspace.get("provisioning_root")
|
|
232
|
+
if provisioning_root is not None and (
|
|
233
|
+
not isinstance(provisioning_root, str)
|
|
234
|
+
or not provisioning_root
|
|
235
|
+
or not Path(provisioning_root).is_absolute()
|
|
236
|
+
):
|
|
237
|
+
raise PlatformConfigurationError("Engineering Workspace Root is invalid.")
|
|
238
|
+
identity = PlatformIdentity(platform["id"], platform["name"], platform["version"], platform["generation"], platform["documentation_namespace"], platform["capability_registry_version"])
|
|
239
|
+
if identity.id != "engineering-platform" or identity.version != CURRENT_PLATFORM_VERSION or identity.generation != 2:
|
|
240
|
+
raise PlatformConfigurationError("Engineering Platform identity is incompatible.")
|
|
241
|
+
authorization = _authorization_policy(workspace, provisioning_root)
|
|
242
|
+
return cls(int(raw["schema_version"]), identity, WorkspaceIdentity(workspace["id"], workspace["name"], repository["provider"], repository["owner"], repository["name"], repository["default_branch"], branding["dashboard_title"], provisioning_root), dict(providers), authorization)
|
|
243
|
+
|
|
244
|
+
def resolve_allowed_workspace_roots(self) -> tuple[AllowedWorkspaceRoot, ...]:
|
|
245
|
+
return self.repository_authorization.allowed_roots
|
|
246
|
+
|
|
247
|
+
def resolve_repository_authorization_policy(self) -> RepositoryAuthorizationPolicy:
|
|
248
|
+
return self.repository_authorization
|
|
249
|
+
|
|
250
|
+
def resolver(self, root: Path) -> ExecutionHostConfigurationResolver:
|
|
251
|
+
return ExecutionHostConfigurationResolver(root, self)
|
|
252
|
+
|
|
253
|
+
def authorize_target_repository(self, path: Path, execution_mode: str) -> RepositoryAuthorization:
|
|
254
|
+
"""Authorize one existing target without broadening host filesystem access."""
|
|
255
|
+
policy = self.repository_authorization
|
|
256
|
+
raw = path.expanduser()
|
|
257
|
+
if not raw.is_absolute():
|
|
258
|
+
return _denied(None, "Target repository path must be absolute.", "Resubmit with an absolute repository path.")
|
|
259
|
+
if ".." in raw.parts:
|
|
260
|
+
return _denied(None, "Target repository path contains traversal.", "Resubmit with a canonical repository path without '..'.")
|
|
261
|
+
if policy.symlink_policy == "reject" and _has_untrusted_symlink_component(raw, policy):
|
|
262
|
+
return _denied(None, "Target repository contains a symlink and the policy rejects symlinks.", "Use a non-symlink repository path or change trusted host configuration.")
|
|
263
|
+
try:
|
|
264
|
+
target = raw.resolve(strict=True)
|
|
265
|
+
except OSError:
|
|
266
|
+
return _denied(None, "Target repository cannot be resolved canonically.", "Correct the target path and retry.")
|
|
267
|
+
if not target.is_dir():
|
|
268
|
+
return _denied(str(target), "Target repository is not a directory.", "Select an existing repository directory.")
|
|
269
|
+
if execution_mode == "MANAGED":
|
|
270
|
+
return RepositoryAuthorization(True, str(target), "managed-host", "managed", "Managed target is the configured execution host.", "No action required.")
|
|
271
|
+
denied = _matching_path(target, policy.denied_repositories, policy.case_sensitivity)
|
|
272
|
+
if denied:
|
|
273
|
+
return _denied(str(target), "Target repository is explicitly denied by trusted host configuration.", "Remove the deny-list entry only when the target is approved.")
|
|
274
|
+
allowed = _matching_path(target, policy.allowed_repositories, policy.case_sensitivity)
|
|
275
|
+
if allowed:
|
|
276
|
+
return RepositoryAuthorization(True, str(target), f"allow-list:{allowed.name}", "explicit_allow_list", "Target repository is explicitly authorized.", "No action required.")
|
|
277
|
+
for root in policy.allowed_roots:
|
|
278
|
+
try:
|
|
279
|
+
canonical_root = _canonical_root(root.path)
|
|
280
|
+
except OSError:
|
|
281
|
+
continue
|
|
282
|
+
if _under_scope(target, canonical_root, root.repository_scope, policy.case_sensitivity):
|
|
283
|
+
return RepositoryAuthorization(True, str(target), f"root:{canonical_root.name}", root.repository_scope, "Target repository matches an authorized workspace root.", "No action required.")
|
|
284
|
+
return _denied(str(target), "Target repository does not match an authorized workspace root or allow-list entry.", "Add an approved root or explicit repository entry in trusted host configuration, then retry.")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _authorization_policy(workspace: dict[str, object], provisioning_root: str | None) -> RepositoryAuthorizationPolicy:
|
|
288
|
+
raw = workspace.get("workspace_authorization")
|
|
289
|
+
if raw is None:
|
|
290
|
+
roots = () if provisioning_root is None else (AllowedWorkspaceRoot(provisioning_root, "direct_children"),)
|
|
291
|
+
return RepositoryAuthorizationPolicy(roots, (), (), "reject", "host", legacy=True)
|
|
292
|
+
if not isinstance(raw, dict) or set(raw) - {"allowed_roots", "allowed_repositories", "denied_repositories", "symlink_policy", "case_sensitivity"}:
|
|
293
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization is invalid.")
|
|
294
|
+
roots_raw = raw.get("allowed_roots", [])
|
|
295
|
+
allow_raw = raw.get("allowed_repositories", [])
|
|
296
|
+
deny_raw = raw.get("denied_repositories", [])
|
|
297
|
+
if not isinstance(roots_raw, list) or not isinstance(allow_raw, list) or not isinstance(deny_raw, list):
|
|
298
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization is invalid.")
|
|
299
|
+
roots: list[AllowedWorkspaceRoot] = []
|
|
300
|
+
for entry in roots_raw:
|
|
301
|
+
if not isinstance(entry, dict) or set(entry) != {"path", "repository_scope"}:
|
|
302
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization root is invalid.")
|
|
303
|
+
path, scope = entry["path"], entry["repository_scope"]
|
|
304
|
+
if not isinstance(path, str) or not Path(path).is_absolute() or scope not in {"direct_children", "descendants"}:
|
|
305
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization root is invalid.")
|
|
306
|
+
roots.append(AllowedWorkspaceRoot(path, scope))
|
|
307
|
+
if not all(isinstance(path, str) and Path(path).is_absolute() for path in (*allow_raw, *deny_raw)):
|
|
308
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization repository list is invalid.")
|
|
309
|
+
symlink_policy = raw.get("symlink_policy", "reject")
|
|
310
|
+
case_sensitivity = raw.get("case_sensitivity", "host")
|
|
311
|
+
if symlink_policy not in {"reject", "canonicalize_within_root"} or case_sensitivity not in {"host", "sensitive"}:
|
|
312
|
+
raise PlatformConfigurationError("Engineering Workspace Authorization is invalid.")
|
|
313
|
+
return RepositoryAuthorizationPolicy(tuple(roots), tuple(allow_raw), tuple(deny_raw), symlink_policy, case_sensitivity)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _canonical_root(value: str) -> Path:
|
|
317
|
+
root = Path(value)
|
|
318
|
+
if root.is_symlink() or not root.is_dir() or root == Path(root.anchor):
|
|
319
|
+
raise OSError("invalid workspace root")
|
|
320
|
+
return root.resolve(strict=True)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _has_untrusted_symlink_component(path: Path, policy: RepositoryAuthorizationPolicy) -> bool:
|
|
324
|
+
"""Reject symlinks below a configured path, not host-owned mount aliases.
|
|
325
|
+
|
|
326
|
+
macOS commonly exposes temporary directories through `/var`, itself a
|
|
327
|
+
system symlink. A trusted configured root may therefore contain such a
|
|
328
|
+
host alias. Only components below that exact configured root are untrusted.
|
|
329
|
+
"""
|
|
330
|
+
configured_paths = [Path(root.path).expanduser() for root in policy.allowed_roots]
|
|
331
|
+
configured_paths.extend(Path(value).expanduser() for value in policy.allowed_repositories)
|
|
332
|
+
configured_paths.extend(Path(value).expanduser() for value in policy.denied_repositories)
|
|
333
|
+
for configured in configured_paths:
|
|
334
|
+
try:
|
|
335
|
+
relative = path.relative_to(configured)
|
|
336
|
+
except ValueError:
|
|
337
|
+
continue
|
|
338
|
+
current = configured
|
|
339
|
+
for part in relative.parts:
|
|
340
|
+
current /= part
|
|
341
|
+
if current.is_symlink():
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _matching_path(target: Path, configured: tuple[str, ...], case_sensitivity: str) -> Path | None:
|
|
347
|
+
for value in configured:
|
|
348
|
+
try:
|
|
349
|
+
candidate = Path(value).resolve(strict=True)
|
|
350
|
+
except OSError:
|
|
351
|
+
continue
|
|
352
|
+
if _same_path(candidate, target, case_sensitivity):
|
|
353
|
+
return candidate
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _same_path(left: Path, right: Path, case_sensitivity: str) -> bool:
|
|
358
|
+
return _path_parts(left, case_sensitivity) == _path_parts(right, case_sensitivity)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _under_scope(target: Path, root: Path, scope: str, case_sensitivity: str) -> bool:
|
|
362
|
+
target_parts = _path_parts(target, case_sensitivity)
|
|
363
|
+
root_parts = _path_parts(root, case_sensitivity)
|
|
364
|
+
if len(target_parts) <= len(root_parts) or target_parts[:len(root_parts)] != root_parts:
|
|
365
|
+
return False
|
|
366
|
+
depth = len(target_parts) - len(root_parts)
|
|
367
|
+
return scope == "descendants" or depth == 1
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _path_parts(path: Path, case_sensitivity: str) -> tuple[str, ...]:
|
|
371
|
+
parts = path.parts
|
|
372
|
+
# APFS normally has case-insensitive semantics; explicit `sensitive` keeps
|
|
373
|
+
# exact component matching for hosts and volumes that require it.
|
|
374
|
+
if case_sensitivity == "host" and sys.platform == "darwin":
|
|
375
|
+
return tuple(part.casefold() for part in parts)
|
|
376
|
+
return parts
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _denied(target: str | None, reason: str, recovery: str) -> RepositoryAuthorization:
|
|
380
|
+
return RepositoryAuthorization(False, target, None, None, reason, recovery)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def resolve_allowed_workspace_roots(root: Path) -> tuple[AllowedWorkspaceRoot, ...]:
|
|
384
|
+
return PlatformConfiguration.load(root).resolve_allowed_workspace_roots()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def resolve_repository_authorization_policy(root: Path) -> RepositoryAuthorizationPolicy:
|
|
388
|
+
return PlatformConfiguration.load(root).resolve_repository_authorization_policy()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def authorize_target_repository(root: Path, path: Path, execution_mode: str) -> RepositoryAuthorization:
|
|
392
|
+
return PlatformConfiguration.load(root).authorize_target_repository(path, execution_mode)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def execution_host_configuration(root: Path) -> ExecutionHostConfigurationResolver:
|
|
396
|
+
"""Return the canonical resolver; consumers must not derive host paths."""
|
|
397
|
+
return PlatformConfiguration.load(root).resolver(root)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _merge(base: dict[str, object], override: dict[str, object]) -> dict[str, object]:
|
|
401
|
+
result = deepcopy(base)
|
|
402
|
+
for key, value in override.items():
|
|
403
|
+
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
404
|
+
result[key] = _merge(result[key], value)
|
|
405
|
+
else:
|
|
406
|
+
result[key] = value
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def capabilities() -> tuple[str, ...]:
|
|
411
|
+
"""Deterministic public capability registry for the 1.5 product boundary."""
|
|
412
|
+
return ("runner", "runtime_provider", "repository_provider", "service_manager_provider", "remote_submission_provider", "private_remote_access_provider", "dashboard", "qualification", "repository_handoff")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def provider_registry(root: Path) -> dict[str, object]:
|
|
416
|
+
"""Public provider discovery API; unknown or unavailable providers are explicit."""
|
|
417
|
+
configuration = PlatformConfiguration.load(root)
|
|
418
|
+
active = registry(root)
|
|
419
|
+
if set(active) != {"runtime", "repository", "service_manager", "remote_submission", "private_remote_access"}:
|
|
420
|
+
raise PlatformConfigurationError("Provider registry is incomplete.")
|
|
421
|
+
return {kind: {"selected": configuration.providers[kind], "status": active[kind]} for kind in active}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def bootstrap_repository(root: Path) -> dict[str, object]:
|
|
425
|
+
"""Public idempotent consumer bootstrap; never changes product source."""
|
|
426
|
+
from .platform_bootstrap import provision_workspace, validate_repository
|
|
427
|
+
configuration = validate_repository(root)
|
|
428
|
+
return {"platform": configuration.platform.id, "workspace": configuration.workspace.id, "provisioned": {name: str(path) for name, path in provision_workspace(root).items()}}
|