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,391 @@
|
|
|
1
|
+
"""Deterministic, transport-free EP consumer v1 contract values.
|
|
2
|
+
|
|
3
|
+
This module defines only the consumer boundary. It neither dispatches an
|
|
4
|
+
operation nor authenticates a credential, opens a listener, reads storage, or
|
|
5
|
+
depends on a consumer repository.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import unicodedata
|
|
14
|
+
from typing import Any, Mapping
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EP_CONSUMER_CONTRACT_VERSION = "1.0"
|
|
18
|
+
EP_CONSUMER_REQUEST_TYPE = "contract.foundation"
|
|
19
|
+
MAX_PROJECT_ID_LENGTH = 128
|
|
20
|
+
MAX_CONSUMER_ID_LENGTH = 128
|
|
21
|
+
MAX_REQUEST_ID_LENGTH = 128
|
|
22
|
+
MAX_CREDENTIAL_LENGTH = 4096
|
|
23
|
+
MAX_PAYLOAD_BYTES = 8192
|
|
24
|
+
|
|
25
|
+
_IDENTIFIER_PATTERN = re.compile(r"[a-z][a-z0-9-]*\Z")
|
|
26
|
+
_CREDENTIAL_PATTERN = re.compile(r"[\x21-\x7e]+\Z")
|
|
27
|
+
_REQUEST_FIELDS = frozenset(
|
|
28
|
+
{"contract_version", "request_type", "request_id", "project_id", "consumer", "auth", "payload"}
|
|
29
|
+
)
|
|
30
|
+
_CONSUMER_FIELDS = frozenset({"consumer_id"})
|
|
31
|
+
_AUTH_FIELDS = frozenset({"scheme", "credential"})
|
|
32
|
+
_RESPONSE_FIELDS = frozenset({"contract_version", "request_id", "status", "payload"})
|
|
33
|
+
_ERROR_ENVELOPE_FIELDS = frozenset({"contract_version", "request_id", "status", "error"})
|
|
34
|
+
_ERROR_FIELDS = frozenset({"code", "message", "field", "path"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ErrorCode:
|
|
38
|
+
"""Stable, public EP consumer v1 error codes."""
|
|
39
|
+
|
|
40
|
+
INVALID_CONTRACT_VERSION = "INVALID_CONTRACT_VERSION"
|
|
41
|
+
MISSING_PROJECT_ID = "MISSING_PROJECT_ID"
|
|
42
|
+
INVALID_PROJECT_ID = "INVALID_PROJECT_ID"
|
|
43
|
+
INVALID_CONSUMER_IDENTITY = "INVALID_CONSUMER_IDENTITY"
|
|
44
|
+
INVALID_AUTH_ENVELOPE = "INVALID_AUTH_ENVELOPE"
|
|
45
|
+
UNKNOWN_FIELD = "UNKNOWN_FIELD"
|
|
46
|
+
UNSUPPORTED_REQUEST_TYPE = "UNSUPPORTED_REQUEST_TYPE"
|
|
47
|
+
INVALID_NORMALIZATION = "INVALID_NORMALIZATION"
|
|
48
|
+
VALUE_TOO_LARGE = "VALUE_TOO_LARGE"
|
|
49
|
+
MALFORMED_REQUEST = "MALFORMED_REQUEST"
|
|
50
|
+
UNAUTHENTICATED = "UNAUTHENTICATED"
|
|
51
|
+
PROJECT_NOT_AUTHORIZED = "PROJECT_NOT_AUTHORIZED"
|
|
52
|
+
SERVICE_NOT_READY = "SERVICE_NOT_READY"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_MESSAGES = {
|
|
56
|
+
ErrorCode.INVALID_CONTRACT_VERSION: "The EP consumer contract version is invalid or unsupported.",
|
|
57
|
+
ErrorCode.MISSING_PROJECT_ID: "project_id is required.",
|
|
58
|
+
ErrorCode.INVALID_PROJECT_ID: "project_id is invalid.",
|
|
59
|
+
ErrorCode.INVALID_CONSUMER_IDENTITY: "consumer identity is invalid.",
|
|
60
|
+
ErrorCode.INVALID_AUTH_ENVELOPE: "authentication envelope is invalid.",
|
|
61
|
+
ErrorCode.UNKNOWN_FIELD: "The envelope contains an unknown field.",
|
|
62
|
+
ErrorCode.UNSUPPORTED_REQUEST_TYPE: "request_type is unsupported.",
|
|
63
|
+
ErrorCode.INVALID_NORMALIZATION: "The value is not in canonical normalized form.",
|
|
64
|
+
ErrorCode.VALUE_TOO_LARGE: "The value exceeds the contract limit.",
|
|
65
|
+
ErrorCode.MALFORMED_REQUEST: "The request is malformed.",
|
|
66
|
+
ErrorCode.UNAUTHENTICATED: "Authentication is required or invalid.",
|
|
67
|
+
ErrorCode.PROJECT_NOT_AUTHORIZED: "The credential is not authorized for this project.",
|
|
68
|
+
ErrorCode.SERVICE_NOT_READY: "The EP consumer boundary is not ready.",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ContractError(ValueError):
|
|
74
|
+
"""A safe, stable validation failure; submitted values are never retained."""
|
|
75
|
+
|
|
76
|
+
code: str
|
|
77
|
+
field: str | None = None
|
|
78
|
+
path: str | None = None
|
|
79
|
+
|
|
80
|
+
def __post_init__(self) -> None:
|
|
81
|
+
if self.code not in _MESSAGES:
|
|
82
|
+
raise ValueError("unknown EP consumer error code")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def message(self) -> str:
|
|
86
|
+
return _MESSAGES[self.code]
|
|
87
|
+
|
|
88
|
+
def __str__(self) -> str:
|
|
89
|
+
return self.message
|
|
90
|
+
|
|
91
|
+
def to_error_envelope(self, request_id: str | None = None) -> "ErrorEnvelope":
|
|
92
|
+
return ErrorEnvelope(
|
|
93
|
+
request_id=request_id, code=self.code, field=self.field, path=self.path
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _require_mapping(value: object, *, code: str, field: str) -> Mapping[str, Any]:
|
|
98
|
+
if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value):
|
|
99
|
+
raise ContractError(code, field=field, path=field)
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _reject_unknown_fields(value: Mapping[str, Any], allowed: frozenset[str], *, path: str) -> None:
|
|
104
|
+
if set(value) - allowed:
|
|
105
|
+
# Never reflect arbitrary submitted keys into a diagnostic: a key is
|
|
106
|
+
# itself consumer-controlled and could carry sensitive text.
|
|
107
|
+
raise ContractError(ErrorCode.UNKNOWN_FIELD, field=path, path=path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _identifier(
|
|
111
|
+
value: object, *, field: str, invalid_code: str, missing_code: str | None = None, limit: int
|
|
112
|
+
) -> str:
|
|
113
|
+
if value is None and missing_code:
|
|
114
|
+
raise ContractError(missing_code, field=field, path=field)
|
|
115
|
+
if not isinstance(value, str) or not value:
|
|
116
|
+
raise ContractError(invalid_code, field=field, path=field)
|
|
117
|
+
if len(value) > limit:
|
|
118
|
+
raise ContractError(ErrorCode.VALUE_TOO_LARGE, field=field, path=field)
|
|
119
|
+
if unicodedata.normalize("NFC", value) != value:
|
|
120
|
+
raise ContractError(ErrorCode.INVALID_NORMALIZATION, field=field, path=field)
|
|
121
|
+
if not _IDENTIFIER_PATTERN.fullmatch(value):
|
|
122
|
+
raise ContractError(invalid_code, field=field, path=field)
|
|
123
|
+
return value
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_payload(value: object, *, path: str = "payload", depth: int = 0) -> object:
|
|
127
|
+
if depth > 4:
|
|
128
|
+
raise ContractError(ErrorCode.VALUE_TOO_LARGE, field="payload", path=path)
|
|
129
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
130
|
+
return value
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
return unicodedata.normalize("NFC", value.replace("\r\n", "\n").replace("\r", "\n"))
|
|
133
|
+
if isinstance(value, list):
|
|
134
|
+
if len(value) > 64:
|
|
135
|
+
raise ContractError(ErrorCode.VALUE_TOO_LARGE, field="payload", path=path)
|
|
136
|
+
return [
|
|
137
|
+
_normalize_payload(item, path=f"{path}[{index}]", depth=depth + 1)
|
|
138
|
+
for index, item in enumerate(value)
|
|
139
|
+
]
|
|
140
|
+
if isinstance(value, Mapping) and all(isinstance(key, str) for key in value):
|
|
141
|
+
if len(value) > 64:
|
|
142
|
+
raise ContractError(ErrorCode.VALUE_TOO_LARGE, field="payload", path=path)
|
|
143
|
+
normalized: dict[str, object] = {}
|
|
144
|
+
for key in sorted(value):
|
|
145
|
+
normalized_key = unicodedata.normalize(
|
|
146
|
+
"NFC", key.replace("\r\n", "\n").replace("\r", "\n")
|
|
147
|
+
)
|
|
148
|
+
if not normalized_key or normalized_key in normalized:
|
|
149
|
+
raise ContractError(ErrorCode.INVALID_NORMALIZATION, field="payload", path=path)
|
|
150
|
+
normalized[normalized_key] = _normalize_payload(
|
|
151
|
+
value[key], path=f"{path}.{normalized_key}", depth=depth + 1
|
|
152
|
+
)
|
|
153
|
+
return normalized
|
|
154
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST, field="payload", path=path)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _payload(value: object) -> dict[str, object]:
|
|
158
|
+
payload = _require_mapping(value, code=ErrorCode.MALFORMED_REQUEST, field="payload")
|
|
159
|
+
normalized = _normalize_payload(payload)
|
|
160
|
+
assert isinstance(normalized, dict)
|
|
161
|
+
try:
|
|
162
|
+
encoded = json.dumps(
|
|
163
|
+
normalized, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
164
|
+
).encode("utf-8")
|
|
165
|
+
except (TypeError, ValueError):
|
|
166
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST, field="payload", path="payload") from None
|
|
167
|
+
if len(encoded) > MAX_PAYLOAD_BYTES:
|
|
168
|
+
raise ContractError(ErrorCode.VALUE_TOO_LARGE, field="payload", path="payload")
|
|
169
|
+
return normalized
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class ConsumerEnvelope:
|
|
174
|
+
consumer_id: str
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def parse(cls, value: object) -> "ConsumerEnvelope":
|
|
178
|
+
consumer = _require_mapping(
|
|
179
|
+
value, code=ErrorCode.INVALID_CONSUMER_IDENTITY, field="consumer"
|
|
180
|
+
)
|
|
181
|
+
_reject_unknown_fields(consumer, _CONSUMER_FIELDS, path="consumer")
|
|
182
|
+
return cls(
|
|
183
|
+
consumer_id=_identifier(
|
|
184
|
+
consumer.get("consumer_id"),
|
|
185
|
+
field="consumer_id",
|
|
186
|
+
invalid_code=ErrorCode.INVALID_CONSUMER_IDENTITY,
|
|
187
|
+
limit=MAX_CONSUMER_ID_LENGTH,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict[str, str]:
|
|
192
|
+
return {"consumer_id": self.consumer_id}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class AuthEnvelope:
|
|
197
|
+
scheme: str
|
|
198
|
+
credential: str
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def parse(cls, value: object) -> "AuthEnvelope":
|
|
202
|
+
auth = _require_mapping(value, code=ErrorCode.INVALID_AUTH_ENVELOPE, field="auth")
|
|
203
|
+
_reject_unknown_fields(auth, _AUTH_FIELDS, path="auth")
|
|
204
|
+
scheme, credential = auth.get("scheme"), auth.get("credential")
|
|
205
|
+
if scheme != "bearer" or not isinstance(credential, str) or not credential:
|
|
206
|
+
raise ContractError(ErrorCode.INVALID_AUTH_ENVELOPE, field="auth", path="auth")
|
|
207
|
+
if len(credential) > MAX_CREDENTIAL_LENGTH:
|
|
208
|
+
raise ContractError(
|
|
209
|
+
ErrorCode.VALUE_TOO_LARGE, field="credential", path="auth.credential"
|
|
210
|
+
)
|
|
211
|
+
if not _CREDENTIAL_PATTERN.fullmatch(credential):
|
|
212
|
+
raise ContractError(
|
|
213
|
+
ErrorCode.INVALID_AUTH_ENVELOPE, field="credential", path="auth.credential"
|
|
214
|
+
)
|
|
215
|
+
return cls(scheme=scheme, credential=credential)
|
|
216
|
+
|
|
217
|
+
def to_dict(self, *, safe: bool = False) -> dict[str, str]:
|
|
218
|
+
return {"scheme": self.scheme, "credential": "[REDACTED]" if safe else self.credential}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True)
|
|
222
|
+
class RequestEnvelope:
|
|
223
|
+
contract_version: str
|
|
224
|
+
request_type: str
|
|
225
|
+
request_id: str
|
|
226
|
+
project_id: str
|
|
227
|
+
consumer: ConsumerEnvelope
|
|
228
|
+
auth: AuthEnvelope
|
|
229
|
+
payload: dict[str, object]
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def parse(cls, value: object) -> "RequestEnvelope":
|
|
233
|
+
request = _require_mapping(value, code=ErrorCode.MALFORMED_REQUEST, field="request")
|
|
234
|
+
_reject_unknown_fields(request, _REQUEST_FIELDS, path="request")
|
|
235
|
+
version = request.get("contract_version")
|
|
236
|
+
if version != EP_CONSUMER_CONTRACT_VERSION:
|
|
237
|
+
raise ContractError(
|
|
238
|
+
ErrorCode.INVALID_CONTRACT_VERSION,
|
|
239
|
+
field="contract_version",
|
|
240
|
+
path="contract_version",
|
|
241
|
+
)
|
|
242
|
+
request_type = request.get("request_type")
|
|
243
|
+
if request_type != EP_CONSUMER_REQUEST_TYPE:
|
|
244
|
+
raise ContractError(
|
|
245
|
+
ErrorCode.UNSUPPORTED_REQUEST_TYPE, field="request_type", path="request_type"
|
|
246
|
+
)
|
|
247
|
+
request_id = _identifier(
|
|
248
|
+
request.get("request_id"),
|
|
249
|
+
field="request_id",
|
|
250
|
+
invalid_code=ErrorCode.MALFORMED_REQUEST,
|
|
251
|
+
limit=MAX_REQUEST_ID_LENGTH,
|
|
252
|
+
)
|
|
253
|
+
return cls(
|
|
254
|
+
contract_version=version,
|
|
255
|
+
request_type=request_type,
|
|
256
|
+
request_id=request_id,
|
|
257
|
+
project_id=_identifier(
|
|
258
|
+
request.get("project_id"),
|
|
259
|
+
field="project_id",
|
|
260
|
+
invalid_code=ErrorCode.INVALID_PROJECT_ID,
|
|
261
|
+
missing_code=ErrorCode.MISSING_PROJECT_ID,
|
|
262
|
+
limit=MAX_PROJECT_ID_LENGTH,
|
|
263
|
+
),
|
|
264
|
+
consumer=ConsumerEnvelope.parse(request.get("consumer")),
|
|
265
|
+
auth=AuthEnvelope.parse(request.get("auth")),
|
|
266
|
+
payload=_payload(request.get("payload")),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def to_dict(self, *, safe: bool = False) -> dict[str, object]:
|
|
270
|
+
return {
|
|
271
|
+
"contract_version": self.contract_version,
|
|
272
|
+
"request_type": self.request_type,
|
|
273
|
+
"request_id": self.request_id,
|
|
274
|
+
"project_id": self.project_id,
|
|
275
|
+
"consumer": self.consumer.to_dict(),
|
|
276
|
+
"auth": self.auth.to_dict(safe=safe),
|
|
277
|
+
"payload": self.payload,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
def serialize(self) -> str:
|
|
281
|
+
return serialize(self.to_dict())
|
|
282
|
+
|
|
283
|
+
def safe_dict(self) -> dict[str, object]:
|
|
284
|
+
return self.to_dict(safe=True)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(frozen=True)
|
|
288
|
+
class ResponseEnvelope:
|
|
289
|
+
request_id: str
|
|
290
|
+
payload: dict[str, object]
|
|
291
|
+
contract_version: str = EP_CONSUMER_CONTRACT_VERSION
|
|
292
|
+
status: str = "success"
|
|
293
|
+
|
|
294
|
+
def __post_init__(self) -> None:
|
|
295
|
+
if self.contract_version != EP_CONSUMER_CONTRACT_VERSION or self.status != "success":
|
|
296
|
+
raise ContractError(ErrorCode.INVALID_CONTRACT_VERSION, field="contract_version")
|
|
297
|
+
_identifier(
|
|
298
|
+
self.request_id,
|
|
299
|
+
field="request_id",
|
|
300
|
+
invalid_code=ErrorCode.MALFORMED_REQUEST,
|
|
301
|
+
limit=MAX_REQUEST_ID_LENGTH,
|
|
302
|
+
)
|
|
303
|
+
object.__setattr__(self, "payload", _payload(self.payload))
|
|
304
|
+
|
|
305
|
+
def to_dict(self) -> dict[str, object]:
|
|
306
|
+
return {
|
|
307
|
+
"contract_version": self.contract_version,
|
|
308
|
+
"request_id": self.request_id,
|
|
309
|
+
"status": self.status,
|
|
310
|
+
"payload": self.payload,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def serialize(self) -> str:
|
|
314
|
+
return serialize(self.to_dict())
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass(frozen=True)
|
|
318
|
+
class ErrorEnvelope:
|
|
319
|
+
request_id: str | None
|
|
320
|
+
code: str
|
|
321
|
+
field: str | None = None
|
|
322
|
+
path: str | None = None
|
|
323
|
+
contract_version: str = EP_CONSUMER_CONTRACT_VERSION
|
|
324
|
+
status: str = "error"
|
|
325
|
+
|
|
326
|
+
def __post_init__(self) -> None:
|
|
327
|
+
if (
|
|
328
|
+
self.contract_version != EP_CONSUMER_CONTRACT_VERSION
|
|
329
|
+
or self.status != "error"
|
|
330
|
+
or self.code not in _MESSAGES
|
|
331
|
+
):
|
|
332
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST)
|
|
333
|
+
if self.request_id is not None:
|
|
334
|
+
_identifier(
|
|
335
|
+
self.request_id,
|
|
336
|
+
field="request_id",
|
|
337
|
+
invalid_code=ErrorCode.MALFORMED_REQUEST,
|
|
338
|
+
limit=MAX_REQUEST_ID_LENGTH,
|
|
339
|
+
)
|
|
340
|
+
for value in (self.field, self.path):
|
|
341
|
+
if value is not None and (
|
|
342
|
+
not isinstance(value, str)
|
|
343
|
+
or len(value) > 128
|
|
344
|
+
or not re.fullmatch(r"[a-z][a-z0-9_.\[\]]*", value)
|
|
345
|
+
):
|
|
346
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST)
|
|
347
|
+
|
|
348
|
+
def to_dict(self) -> dict[str, object]:
|
|
349
|
+
error: dict[str, object] = {"code": self.code, "message": _MESSAGES[self.code]}
|
|
350
|
+
if self.field is not None:
|
|
351
|
+
error["field"] = self.field
|
|
352
|
+
if self.path is not None:
|
|
353
|
+
error["path"] = self.path
|
|
354
|
+
return {
|
|
355
|
+
"contract_version": self.contract_version,
|
|
356
|
+
"request_id": self.request_id,
|
|
357
|
+
"status": self.status,
|
|
358
|
+
"error": error,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def serialize(self) -> str:
|
|
362
|
+
return serialize(self.to_dict())
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def serialize(value: object) -> str:
|
|
366
|
+
"""Return canonical JSON for an already validated contract value."""
|
|
367
|
+
if isinstance(value, (RequestEnvelope, ResponseEnvelope, ErrorEnvelope)):
|
|
368
|
+
value = value.to_dict()
|
|
369
|
+
return json.dumps(
|
|
370
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def deserialize_request(serialized: str | bytes | bytearray) -> RequestEnvelope:
|
|
375
|
+
"""Decode JSON without duplicate-key ambiguity, then validate the request."""
|
|
376
|
+
|
|
377
|
+
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
378
|
+
result: dict[str, object] = {}
|
|
379
|
+
for key, item in pairs:
|
|
380
|
+
if key in result:
|
|
381
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST, field=key, path=key)
|
|
382
|
+
result[key] = item
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
decoded = json.loads(serialized, object_pairs_hook=reject_duplicate_keys)
|
|
387
|
+
except ContractError:
|
|
388
|
+
raise
|
|
389
|
+
except (TypeError, UnicodeDecodeError, json.JSONDecodeError):
|
|
390
|
+
raise ContractError(ErrorCode.MALFORMED_REQUEST) from None
|
|
391
|
+
return RequestEnvelope.parse(decoded)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""JSON-compatible, versioned public contract value objects.
|
|
2
|
+
|
|
3
|
+
These objects describe evidence and policy decisions only. They contain no
|
|
4
|
+
filesystem locations, executable commands, provider output, or prompt text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CONTRACT_MAJOR_VERSION = 1
|
|
14
|
+
CONTRACT_VERSION = "1.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContractVersionError(ValueError):
|
|
18
|
+
"""Raised when an incompatible external contract version is requested."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require_compatible_version(version: object) -> None:
|
|
22
|
+
"""Fail closed for unknown major contract versions."""
|
|
23
|
+
if not isinstance(version, str) or version.split(".", 1)[0] != str(CONTRACT_MAJOR_VERSION):
|
|
24
|
+
raise ContractVersionError("Engineering contract version is incompatible.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class _ContractValue:
|
|
29
|
+
contract_version: str = CONTRACT_VERSION
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
require_compatible_version(self.contract_version)
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class EvidenceReference(_ContractValue):
|
|
38
|
+
"""A bounded pointer to canonical evidence, never the evidence payload."""
|
|
39
|
+
|
|
40
|
+
id: str = ""
|
|
41
|
+
source_type: str = "UNAVAILABLE"
|
|
42
|
+
authority: str = "UNAVAILABLE"
|
|
43
|
+
observed_at: str = "UNAVAILABLE"
|
|
44
|
+
freshness: str = "UNKNOWN"
|
|
45
|
+
subject: str = "UNAVAILABLE"
|
|
46
|
+
snapshot_identity: str = "UNAVAILABLE"
|
|
47
|
+
safe_summary_code: str = "UNAVAILABLE"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class AllowedAction(_ContractValue):
|
|
52
|
+
"""A capability descriptor; it intentionally has no executable payload."""
|
|
53
|
+
|
|
54
|
+
action_id: str = ""
|
|
55
|
+
action_namespace: str = ""
|
|
56
|
+
action_version: str = CONTRACT_VERSION
|
|
57
|
+
run_id: str = ""
|
|
58
|
+
target_identity: str = "RUN"
|
|
59
|
+
classification: str = "READ_ONLY"
|
|
60
|
+
mutation_level: str = "NONE"
|
|
61
|
+
authority_required: str = "EP_POLICY"
|
|
62
|
+
confirmation_required: bool = False
|
|
63
|
+
allowed: bool = False
|
|
64
|
+
reason_code: str = "UNAVAILABLE"
|
|
65
|
+
evidence_version: str = "UNAVAILABLE"
|
|
66
|
+
expires_at: str = "BOUNDARY_SENSITIVE"
|
|
67
|
+
scope: str = "RUN_SCOPED"
|
|
68
|
+
expected_effect_code: str = "READ_CANONICAL_EVIDENCE"
|
|
69
|
+
blocked_reason_code: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ActionPolicyDecision(_ContractValue):
|
|
74
|
+
action_id: str = ""
|
|
75
|
+
run_id: str = ""
|
|
76
|
+
decision: str = "UNAVAILABLE"
|
|
77
|
+
reason_code: str = "UNAVAILABLE"
|
|
78
|
+
policy_version: str = CONTRACT_VERSION
|
|
79
|
+
evaluated_at: str = "UNAVAILABLE"
|
|
80
|
+
evidence_version: str = "UNAVAILABLE"
|
|
81
|
+
authority: str = "EP_POLICY"
|
|
82
|
+
confirmation_required: bool = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class ActionAuditRecord(_ContractValue):
|
|
87
|
+
"""Append-only audit shape for a future governed action gateway.
|
|
88
|
+
|
|
89
|
+
No persistence or execution behaviour is introduced by this declaration.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
audit_id: str = ""
|
|
93
|
+
run_id: str = ""
|
|
94
|
+
action_id: str = ""
|
|
95
|
+
action_version: str = CONTRACT_VERSION
|
|
96
|
+
policy_version: str = CONTRACT_VERSION
|
|
97
|
+
evidence_version: str = "UNAVAILABLE"
|
|
98
|
+
actor_type: str = "UNAVAILABLE"
|
|
99
|
+
authority: str = "UNAVAILABLE"
|
|
100
|
+
requested_at: str = "UNAVAILABLE"
|
|
101
|
+
confirmed_at: str | None = None
|
|
102
|
+
executed_at: str | None = None
|
|
103
|
+
result: str = "UNAVAILABLE"
|
|
104
|
+
resulting_state_code: str = "UNAVAILABLE"
|
|
105
|
+
validation_evidence_reference: str | None = None
|