forgexa-cli 1.78.0__tar.gz → 1.78.2__tar.gz

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 (33) hide show
  1. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/PKG-INFO +3 -1
  2. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/daemon.py +1 -1
  4. forgexa_cli-1.78.2/forgexa_cli/observability_contract.py +833 -0
  5. forgexa_cli-1.78.2/forgexa_cli/observability_evidence.py +132 -0
  6. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/PKG-INFO +3 -1
  7. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/SOURCES.txt +2 -0
  8. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/requires.txt +2 -0
  9. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/pyproject.toml +2 -2
  10. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/README.md +0 -0
  11. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/_build_config.py +0 -0
  12. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/_local_bind.py +0 -0
  13. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/agent_core.py +0 -0
  14. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/autoupgrade.py +0 -0
  15. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/aws_evidence.py +0 -0
  16. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  17. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/main.py +0 -0
  18. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/py.typed +0 -0
  19. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/runtime_evidence.py +0 -0
  20. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli/zcode_protocol.py +0 -0
  21. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  22. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  23. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/setup.cfg +0 -0
  25. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_runtime_credentials.py +0 -0
  31. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_session_credentials.py +0 -0
  32. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_silent_install.py +0 -0
  33. {forgexa_cli-1.78.0 → forgexa_cli-1.78.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.78.0
3
+ Version: 1.78.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -26,6 +26,8 @@ Requires-Python: >=3.9
26
26
  Description-Content-Type: text/markdown
27
27
  Requires-Dist: httpx>=0.24
28
28
  Requires-Dist: socksio>=1.0.0
29
+ Requires-Dist: pydantic<3,>=2.10
30
+ Requires-Dist: pycountry>=24.6
29
31
  Provides-Extra: daemon
30
32
  Requires-Dist: httpx>=0.24; extra == "daemon"
31
33
  Requires-Dist: socksio>=1.0.0; extra == "daemon"
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.78.0"
2
+ __version__ = "1.78.2"
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
1234
1234
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1235
1235
  # Kept in sync with pyproject.toml version via bump-version.sh.
1236
1236
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1237
- DAEMON_VERSION = "1.78.0"
1237
+ DAEMON_VERSION = "1.78.2"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -0,0 +1,833 @@
1
+ """Generated by backend/scripts/export_observability_runtime.py; do not edit."""
2
+ from __future__ import annotations
3
+ from typing import Union
4
+ from itertools import zip_longest
5
+
6
+ def _strict_zip(*iterables):
7
+ missing = object()
8
+ for items in zip_longest(*iterables, fillvalue=missing):
9
+ if any((item is missing for item in items)):
10
+ raise ValueError('zip() arguments have different lengths')
11
+ yield items
12
+ import pycountry
13
+ from pydantic import BaseModel
14
+ from pydantic import ConfigDict
15
+ import math
16
+ from typing import Annotated
17
+ from typing import Literal
18
+ from urllib.parse import urlsplit
19
+ from uuid import UUID
20
+ from pydantic import AwareDatetime
21
+ from pydantic import Field
22
+ from pydantic import field_validator
23
+ from pydantic import model_serializer
24
+ from pydantic import model_validator
25
+ import json
26
+ from datetime import datetime
27
+ Provider = Literal['mpulse', 'newrelic']
28
+ ResultType = Literal['summary', 'table', 'timeseries', 'histogram', 'distribution', 'records', 'span', 'trace', 'metadata']
29
+ Identifier = Annotated[str, Field(min_length=1, max_length=256, pattern='^[A-Za-z0-9_.:/-]+$')]
30
+ Scalar = Union[Union[Union[Annotated[str, Field(max_length=2048)], bool], int], float]
31
+
32
+ class ObservabilityModel(BaseModel):
33
+ model_config = ConfigDict(extra='forbid', allow_inf_nan=False, hide_input_in_errors=True)
34
+
35
+ def unique_values(values: list) -> list:
36
+ if len(values) != len(set(values)):
37
+ raise ValueError('Values must be unique')
38
+ return values
39
+
40
+ def validate_country_code(country: str) -> str:
41
+ if len(country) != 2 or not country.isupper() or pycountry.countries.get(alpha_2=country) is None:
42
+ raise ValueError('Invalid ISO country code')
43
+ return country
44
+
45
+ class ObservabilityWindow(ObservabilityModel):
46
+ start_time: AwareDatetime
47
+ end_time: AwareDatetime
48
+
49
+ @model_validator(mode='after')
50
+ def validate_window(self) -> ObservabilityWindow:
51
+ if self.start_time >= self.end_time:
52
+ raise ValueError('Window must have a positive duration')
53
+ return self
54
+
55
+ class ObservabilitySelection(ObservabilityModel):
56
+ field_id: Union[Identifier, Literal['*']]
57
+ aggregate: Literal['native', 'count', 'sum', 'average', 'mean', 'min', 'max', 'percentile', 'rate', 'uniqueCount', 'apdex', 'histogram']
58
+ percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
59
+ rate_interval_seconds: Union[int, None] = Field(default=None, ge=1, le=86400, strict=True)
60
+ threshold: Union[float, None] = Field(default=None, gt=0, strict=True)
61
+
62
+ @model_validator(mode='after')
63
+ def validate_arguments(self) -> ObservabilitySelection:
64
+ arguments = {'percentile': 'percentile', 'rate_interval_seconds': 'rate', 'threshold': 'apdex'}
65
+ for name, aggregate in arguments.items():
66
+ required = self.aggregate == aggregate
67
+ if required != (getattr(self, name) is not None):
68
+ raise ValueError('Aggregate arguments do not match the selected function')
69
+ if not required and name in self.model_fields_set:
70
+ raise ValueError('Aggregate does not accept this argument')
71
+ if self.field_id == '*' and self.aggregate != 'count':
72
+ raise ValueError('Wildcard fields are allowed only for count')
73
+ return self
74
+
75
+ class ObservabilityCondition(ObservabilityModel):
76
+ field_id: Identifier
77
+ operator: Literal['eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte', 'exists', 'not_exists', 'prefix', 'contains']
78
+ value: Union[Scalar, None] = Field(default=None, repr=False)
79
+ values: Union[list[Scalar], None] = Field(default=None, min_length=1, max_length=100, repr=False)
80
+
81
+ @model_serializer(mode='wrap')
82
+ def serialize_operands(self, handler):
83
+ result = handler(self)
84
+ for name in ('value', 'values'):
85
+ if getattr(self, name) is None:
86
+ result.pop(name, None)
87
+ return result
88
+
89
+ @model_validator(mode='after')
90
+ def validate_operands(self) -> ObservabilityCondition:
91
+ supplied = self.model_fields_set & {'value', 'values'}
92
+ if self.operator in {'exists', 'not_exists'}:
93
+ if supplied:
94
+ raise ValueError('Existence conditions do not accept operands')
95
+ elif self.operator in {'in', 'not_in'}:
96
+ if supplied != {'values'} or self.values is None:
97
+ raise ValueError('Set conditions require non-empty values')
98
+ elif supplied != {'value'} or self.value is None:
99
+ raise ValueError('Comparison conditions require one non-null value')
100
+ if self.operator in {'prefix', 'contains'} and (not isinstance(self.value, str)):
101
+ raise ValueError('Text conditions require a string')
102
+ return self
103
+
104
+ class ObservabilityFilterGroup(ObservabilityModel):
105
+ operator: Literal['and', 'or']
106
+ conditions: list[Annotated[Union[ObservabilityCondition, ObservabilityFilterGroup], Field(discriminator='operator')]] = Field(min_length=1, max_length=100)
107
+ FilterTree = Annotated[Union[ObservabilityCondition, ObservabilityFilterGroup], Field(discriminator='operator')]
108
+
109
+ class ObservabilityOrder(ObservabilityModel):
110
+ field_id: Identifier
111
+ direction: Literal['asc', 'desc']
112
+
113
+ class ObservabilityQuery(ObservabilityModel):
114
+ dataset_id: Identifier
115
+ result_type: ResultType
116
+ select: Union[list[ObservabilitySelection], None] = Field(default=None, min_length=1, max_length=50)
117
+ fields: Union[list[Identifier], None] = Field(default=None, min_length=1, max_length=50)
118
+ filters: Union[FilterTree, None] = None
119
+ group_by: list[Identifier] = Field(default_factory=list, max_length=10)
120
+ order_by: list[ObservabilityOrder] = Field(default_factory=list, max_length=5)
121
+ interval_seconds: Union[int, None] = Field(default=None, ge=1, le=604800, strict=True)
122
+ limit: int = Field(default=100, ge=1, le=5000, strict=True)
123
+
124
+ @field_validator('filters', mode='before')
125
+ @classmethod
126
+ def validate_filter_size(cls, value):
127
+ pending = [(value, 1)] if value is not None else []
128
+ count = 0
129
+ while pending:
130
+ node, depth = pending.pop()
131
+ count += 1
132
+ if depth > 8 or count > 100:
133
+ raise ValueError('Filter tree exceeds its depth or node limit')
134
+ if isinstance(node, dict) and isinstance(node.get('conditions'), list):
135
+ if len(node['conditions']) > 100:
136
+ raise ValueError('Filter tree exceeds its node limit')
137
+ pending.extend(((child, depth + 1) for child in node['conditions']))
138
+ elif isinstance(node, ObservabilityFilterGroup):
139
+ pending.extend(((child, depth + 1) for child in node.conditions))
140
+ return value
141
+
142
+ @model_validator(mode='after')
143
+ def validate_shape(self) -> ObservabilityQuery:
144
+ projected = self.result_type in {'records', 'span', 'trace', 'metadata'}
145
+ if projected:
146
+ if not self.fields or 'select' in self.model_fields_set or self.group_by:
147
+ raise ValueError('Projected results require fields and reject aggregates or grouping')
148
+ unique_values(self.fields)
149
+ elif not self.select or 'fields' in self.model_fields_set:
150
+ raise ValueError('Aggregate results require selections and reject record projections')
151
+ unique_values(self.group_by)
152
+ unique_values([item.field_id for item in self.order_by])
153
+ if self.interval_seconds is not None and self.result_type != 'timeseries':
154
+ raise ValueError('Intervals are allowed only for time series')
155
+ return self
156
+ Sha256 = Annotated[str, Field(pattern='^[a-f0-9]{64}$')]
157
+ CellValue = Union[Union[Union[Union[str, bool], int], float], None]
158
+ CountKind = Literal['measurements', 'aggregate_requests', 'reported_events', 'estimated_events', 'sessions', 'unknown']
159
+ Statistic = Literal['native', 'count', 'sum', 'mean', 'average', 'min', 'max', 'percentile', 'rate', 'uniqueCount', 'apdex']
160
+
161
+ def validate_issuer(value: str) -> str:
162
+ try:
163
+ if any((character.isspace() or ord(character) < 32 for character in value)):
164
+ raise ValueError
165
+ parsed = urlsplit(value)
166
+ parsed.port
167
+ if not parsed.hostname or parsed.username or parsed.password:
168
+ raise ValueError
169
+ if parsed.path not in {'', '/'} or parsed.query or parsed.fragment:
170
+ raise ValueError
171
+ if parsed.scheme != 'https' and (not (parsed.scheme == 'http' and parsed.hostname in {'localhost', '127.0.0.1', '::1'})):
172
+ raise ValueError
173
+ except ValueError:
174
+ raise ValueError('observability_invalid_issuer') from None
175
+ return value
176
+
177
+ def validate_unit_values(native_value, value, native_unit, display_unit):
178
+ if native_value is None or value is None:
179
+ if native_value is not None or value is not None:
180
+ raise ValueError('observability_inconsistent_missing_value')
181
+ return
182
+ conversions = {('seconds', 'milliseconds'): 1000, ('milliseconds', 'seconds'): 0.001, ('ratio', 'percent'): 100, ('percent', 'ratio'): 0.01}
183
+ scale = 1 if native_unit == display_unit else conversions.get((native_unit, display_unit))
184
+ if scale is None or not math.isclose(native_value * scale, value, rel_tol=1e-12, abs_tol=1e-12):
185
+ raise ValueError('observability_unit_conversion_invalid')
186
+
187
+ class ObservabilityOrigin(ObservabilityModel):
188
+ issuer: str = Field(min_length=1, max_length=2048)
189
+ workspace_id: UUID
190
+ project_id: UUID
191
+ created_by_user_id: UUID
192
+ _validate_issuer = field_validator('issuer')(validate_issuer)
193
+
194
+ class ObservabilityCoverageWindow(ObservabilityWindow):
195
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
196
+
197
+ class ObservabilitySource(ObservabilityModel):
198
+ source_id: Identifier
199
+ target_ref: Identifier
200
+ connection_id: Identifier
201
+ environment: Identifier
202
+ endpoint_region: Identifier
203
+ scope_precision: Literal['dedicated', 'filtered']
204
+ origin_scope_ids: list[Identifier] = Field(min_length=1, max_length=100)
205
+ market_codes: list[str] = Field(default_factory=list, max_length=20)
206
+ application_id: Union[Identifier, None] = None
207
+ account_id: Union[int, None] = Field(default=None, ge=1, strict=True)
208
+ entity_guid: Union[str, None] = Field(default=None, min_length=1, max_length=256)
209
+ material_version: int = Field(ge=1, strict=True)
210
+ status: Literal['available', 'zero_matches', 'partial', 'unavailable']
211
+ completed: bool
212
+ actual_windows: list[ObservabilityCoverageWindow] = Field(default_factory=list, max_length=500)
213
+
214
+ class ObservabilitySafeCondition(ObservabilityModel):
215
+ field_id: Identifier
216
+ operator: Literal['eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte', 'exists', 'not_exists', 'prefix', 'contains']
217
+ values_redacted: Literal[True] = True
218
+
219
+ class ObservabilitySafeFilterGroup(ObservabilityModel):
220
+ operator: Literal['and', 'or']
221
+ conditions: list[Annotated[Union[ObservabilitySafeCondition, ObservabilitySafeFilterGroup], Field(discriminator='operator')]] = Field(min_length=1, max_length=100)
222
+
223
+ class ObservabilityFrozenScope(ObservabilityModel):
224
+ scope_mode: Literal['common', 'market']
225
+ scope_ids: list[Identifier] = Field(default_factory=list, max_length=20)
226
+ country_codes: list[str] = Field(default_factory=list, max_length=20)
227
+ environment: Union[Identifier, None] = None
228
+ target_refs: list[Identifier] = Field(min_length=1, max_length=100)
229
+
230
+ @model_validator(mode='after')
231
+ def validate_scope(self) -> ObservabilityFrozenScope:
232
+ for values in (self.scope_ids, self.country_codes, self.target_refs):
233
+ unique_values(values)
234
+ if self.scope_mode == 'common':
235
+ if not self.scope_ids or self.country_codes or self.environment is not None:
236
+ raise ValueError('observability_scope_mode_mismatch')
237
+ elif not self.country_codes or not self.environment or self.scope_ids:
238
+ raise ValueError('observability_scope_mode_mismatch')
239
+ for country in self.country_codes:
240
+ validate_country_code(country)
241
+ return self
242
+
243
+ class ObservabilitySafeParameters(ObservabilityModel):
244
+ dataset_id: Identifier
245
+ request_digest: Sha256
246
+ field_ids: list[Identifier] = Field(min_length=1, max_length=50)
247
+ filter_fields: list[Identifier] = Field(default_factory=list, max_length=100)
248
+ filter_values_redacted: Literal[True] = True
249
+ filters: Union[Annotated[Union[ObservabilitySafeCondition, ObservabilitySafeFilterGroup], Field(discriminator='operator')], None] = None
250
+ query_ref: Identifier
251
+ limit: Union[int, None] = Field(default=None, ge=1, le=5000, strict=True)
252
+ interval_seconds: Union[int, None] = Field(default=None, ge=1, strict=True)
253
+ workload_ids: Union[list[Identifier], None] = Field(default=None, min_length=1, max_length=1)
254
+
255
+ @model_serializer(mode='wrap')
256
+ def serialize_optional_workloads(self, handler):
257
+ value = handler(self)
258
+ if self.workload_ids is None:
259
+ value.pop('workload_ids', None)
260
+ return value
261
+
262
+ @field_validator('filters', mode='before')
263
+ @classmethod
264
+ def validate_filter_size(cls, value):
265
+ if isinstance(value, ObservabilityModel):
266
+ value = value.model_dump()
267
+ return ObservabilityQuery.validate_filter_size(value)
268
+
269
+ def validate_redacted_value(value, mode):
270
+ if value is None or mode == 'none':
271
+ return
272
+ valid = value == '[REDACTED]' if mode == 'mask' else isinstance(value, str) and value.startswith('sha256:') and (len(value) == 71) and all((character in '0123456789abcdef' for character in value[7:]))
273
+ if not valid:
274
+ raise ValueError('observability_redacted_value_invalid')
275
+
276
+ class ObservabilityColumn(ObservabilityModel):
277
+ field_id: Identifier
278
+ value_type: Literal['string', 'number', 'integer', 'boolean', 'timestamp']
279
+ classification: Literal['aggregate', 'identifier', 'text']
280
+ native_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
281
+ display_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
282
+ definition_version: int = Field(default=1, ge=1, strict=True)
283
+ statistic: Union[Statistic, None] = None
284
+ percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
285
+ redaction: Literal['none', 'mask', 'sha256'] = 'none'
286
+ native_value_type: Union[Literal['string', 'number', 'integer', 'boolean', 'timestamp'], None] = None
287
+
288
+ @model_validator(mode='after')
289
+ def validate_statistic(self) -> ObservabilityColumn:
290
+ if (self.statistic == 'percentile') != (self.percentile is not None):
291
+ raise ValueError('observability_percentile_required')
292
+ if (self.redaction == 'none') != (self.native_value_type is None) or (self.redaction != 'none' and self.value_type != 'string'):
293
+ raise ValueError('observability_redacted_column_invalid')
294
+ return self
295
+
296
+ @model_serializer(mode='wrap')
297
+ def serialize_column(self, handler):
298
+ value = handler(self)
299
+ if self.redaction == 'none':
300
+ value.pop('redaction', None)
301
+ value.pop('native_value_type', None)
302
+ return value
303
+
304
+ class ObservabilityMetricQuality(ObservabilityModel):
305
+ status: Literal['available', 'missing', 'unsupported', 'unavailable']
306
+ sampling: Identifier
307
+ actual_interval_seconds: Union[int, None] = Field(default=None, ge=1, strict=True)
308
+ provisional: bool = False
309
+ truncated: bool = False
310
+
311
+ class ObservabilityMetricValue(ObservabilityWindow):
312
+ kind: Literal['metric'] = 'metric'
313
+ evidence_id: Identifier
314
+ integration_type: Provider
315
+ evidence_type: Literal['mpulse_data', 'newrelic_data']
316
+ data_family: Identifier
317
+ result_type: ResultType
318
+ source_id: Identifier
319
+ target_ref: Union[Identifier, None] = None
320
+ scope_id: Identifier
321
+ workload_id: Union[Identifier, None] = None
322
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
323
+ metric_id: Identifier
324
+ definition_version: int = Field(ge=1, strict=True)
325
+ data_type: Identifier
326
+ native_metric: str = Field(min_length=1, max_length=256)
327
+ statistic: Statistic
328
+ percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
329
+ native_unit: str = Field(min_length=1, max_length=128)
330
+ native_value: Union[float, None] = Field(default=None, strict=True)
331
+ display_unit: str = Field(min_length=1, max_length=128)
332
+ value: Union[float, None] = Field(default=None, strict=True)
333
+ sample_count: Union[int, None] = Field(default=None, ge=0, strict=True)
334
+ count_kind: CountKind
335
+ query_ref: Identifier
336
+ quality: ObservabilityMetricQuality
337
+
338
+ @model_validator(mode='after')
339
+ def validate_measurement(self) -> ObservabilityMetricValue:
340
+ if self.evidence_type != f'{self.integration_type}_data':
341
+ raise ValueError('observability_provider_mismatch')
342
+ if (self.statistic == 'percentile') != (self.percentile is not None):
343
+ raise ValueError('observability_percentile_required')
344
+ if self.quality.status == 'available' and self.value is None:
345
+ raise ValueError('observability_available_value_required')
346
+ if self.quality.status != 'available' and self.value is not None:
347
+ raise ValueError('observability_missing_value_must_be_null')
348
+ if self.statistic == 'percentile' and self.value is not None and (self.sample_count == 0):
349
+ raise ValueError('observability_empty_percentile_population')
350
+ validate_unit_values(self.native_value, self.value, self.native_unit, self.display_unit)
351
+ return self
352
+
353
+ class ObservabilityRow(ObservabilityModel):
354
+ kind: Literal['row', 'metadata']
355
+ item_id: Identifier
356
+ target_ref: Identifier
357
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
358
+ values: dict[Identifier, CellValue] = Field(min_length=1, max_length=50)
359
+
360
+ class ObservabilityTimeBucket(ObservabilityWindow):
361
+ item_id: Identifier
362
+ native_value: Union[float, None] = Field(default=None, strict=True)
363
+ value: Union[float, None] = Field(default=None, strict=True)
364
+ sample_count: Union[int, None] = Field(default=None, ge=0, strict=True)
365
+
366
+ class ObservabilitySeries(ObservabilityModel):
367
+ kind: Literal['series']
368
+ item_id: Identifier
369
+ target_ref: Identifier
370
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
371
+ field_id: Identifier
372
+ dimensions: dict[Identifier, CellValue] = Field(default_factory=dict, max_length=10)
373
+ native_unit: str = Field(min_length=1, max_length=128)
374
+ display_unit: str = Field(min_length=1, max_length=128)
375
+ count_kind: CountKind
376
+ statistic: Statistic
377
+ percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
378
+ buckets: list[ObservabilityTimeBucket] = Field(min_length=1, max_length=50000)
379
+
380
+ @model_serializer(mode='wrap')
381
+ def preserve_legacy_series(self, handler):
382
+ result = handler(self)
383
+ if not self.dimensions:
384
+ result.pop('dimensions', None)
385
+ return result
386
+
387
+ @model_validator(mode='after')
388
+ def validate_buckets(self) -> ObservabilitySeries:
389
+ if (self.statistic == 'percentile') != (self.percentile is not None):
390
+ raise ValueError('observability_percentile_required')
391
+ for bucket in self.buckets:
392
+ validate_unit_values(bucket.native_value, bucket.value, self.native_unit, self.display_unit)
393
+ for previous, current in zip(self.buckets, self.buckets[1:]):
394
+ if previous.end_time > current.start_time:
395
+ raise ValueError('observability_time_buckets_overlap')
396
+ return self
397
+
398
+ class ObservabilityHistory(ObservabilityModel):
399
+ kind: Literal['history'] = 'history'
400
+ item_id: Identifier
401
+ target_ref: Identifier
402
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
403
+ field_id: Identifier
404
+ axis: Literal['sample_index'] = 'sample_index'
405
+ data_timezone: Union[str, None] = Field(default=None, max_length=128)
406
+ native_unit: str = Field(min_length=1, max_length=128)
407
+ display_unit: str = Field(min_length=1, max_length=128)
408
+ statistic: Statistic
409
+ percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
410
+ count_kind: CountKind
411
+ native_values: list[Union[float, None]] = Field(max_length=50000)
412
+ values: list[Union[float, None]] = Field(max_length=50000)
413
+ latest_native_value: Union[float, None] = Field(default=None, strict=True)
414
+ latest_value: Union[float, None] = Field(default=None, strict=True)
415
+
416
+ @field_validator('native_values', 'values', mode='before')
417
+ @classmethod
418
+ def numeric_history(cls, values):
419
+ if not isinstance(values, list) or any((value is not None and type(value) not in {int, float} for value in values)):
420
+ raise ValueError('observability_history_value_invalid')
421
+ return values
422
+
423
+ @model_validator(mode='after')
424
+ def validate_history(self):
425
+ if not self.native_values and self.latest_native_value is None:
426
+ raise ValueError('observability_history_empty')
427
+ if len(self.native_values) != len(self.values) or (self.statistic == 'percentile') != (self.percentile is not None):
428
+ raise ValueError('observability_history_semantics_invalid')
429
+ for native_value, value in _strict_zip(self.native_values, self.values):
430
+ validate_unit_values(native_value, value, self.native_unit, self.display_unit)
431
+ validate_unit_values(self.latest_native_value, self.latest_value, self.native_unit, self.display_unit)
432
+ return self
433
+
434
+ class ObservabilityDistributionBucket(ObservabilityModel):
435
+ item_id: Identifier
436
+ lower: Union[float, None] = Field(default=None, strict=True)
437
+ upper: Union[float, None] = Field(default=None, strict=True)
438
+ count: Union[int, None] = Field(default=None, ge=0, strict=True)
439
+ native_value: Union[float, None] = Field(default=None, strict=True)
440
+ value: Union[float, None] = Field(default=None, strict=True)
441
+
442
+ @model_validator(mode='after')
443
+ def validate_edges(self) -> ObservabilityDistributionBucket:
444
+ if self.lower is not None and self.upper is not None and (self.lower >= self.upper):
445
+ raise ValueError('observability_distribution_edges_invalid')
446
+ return self
447
+
448
+ class ObservabilityDistribution(ObservabilityModel):
449
+ kind: Literal['distribution']
450
+ item_id: Identifier
451
+ target_ref: Identifier
452
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
453
+ field_id: Identifier
454
+ measurement_unit: str = Field(min_length=1, max_length=128)
455
+ native_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
456
+ display_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
457
+ count_kind: CountKind
458
+ buckets: list[ObservabilityDistributionBucket] = Field(min_length=1, max_length=50000)
459
+
460
+ @model_validator(mode='after')
461
+ def validate_values(self) -> ObservabilityDistribution:
462
+ for bucket in self.buckets:
463
+ if bucket.value is not None or bucket.native_value is not None:
464
+ if self.native_unit is None or self.display_unit is None:
465
+ raise ValueError('observability_distribution_value_units_required')
466
+ validate_unit_values(bucket.native_value, bucket.value, self.native_unit, self.display_unit)
467
+ return self
468
+
469
+ class ObservabilitySpan(ObservabilityModel):
470
+ kind: Literal['span']
471
+ item_id: Identifier
472
+ target_ref: Identifier
473
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
474
+ trace_id: Identifier
475
+ span_id: Identifier
476
+ parent_span_id: Union[Identifier, None] = None
477
+ parent_missing: bool
478
+ values: dict[Identifier, CellValue] = Field(default_factory=dict, max_length=50)
479
+
480
+ class ObservabilityRedactedItem(ObservabilityModel):
481
+ kind: Literal['redacted']
482
+ item_id: Identifier
483
+ target_ref: Identifier
484
+ window_index: int = Field(default=0, ge=0, le=4, strict=True)
485
+ original_kind: Literal['metric', 'series', 'history', 'distribution', 'span']
486
+ value_scope: Literal['whole_field_structure']
487
+ quality: Union[ObservabilityMetricQuality, None] = None
488
+ primary_field_id: Union[Identifier, None] = None
489
+ bucket_refs: list[Identifier] = Field(default_factory=list, max_length=1000)
490
+ values: dict[Identifier, CellValue] = Field(min_length=1, max_length=50)
491
+
492
+ @model_validator(mode='after')
493
+ def validate_representation(self):
494
+ unique_values(self.bucket_refs)
495
+ if self.original_kind == 'span':
496
+ if self.primary_field_id is not None or self.bucket_refs:
497
+ raise ValueError('observability_redacted_structure_invalid')
498
+ elif self.primary_field_id not in self.values or (self.bucket_refs and self.original_kind not in {'series', 'distribution'}):
499
+ raise ValueError('observability_redacted_structure_invalid')
500
+ return self
501
+ DataItem = Annotated[Union[Union[Union[Union[Union[Union[ObservabilityMetricValue, ObservabilityRow], ObservabilitySeries], ObservabilityHistory], ObservabilityDistribution], ObservabilitySpan], ObservabilityRedactedItem], Field(discriminator='kind')]
502
+
503
+ class ObservabilitySnapshotQuality(ObservabilityModel):
504
+ status: Literal['available', 'zero_matches', 'partial', 'unavailable']
505
+ completed: bool
506
+ sampling: Identifier
507
+ truncated: bool = False
508
+ limitations: list[Identifier] = Field(default_factory=list, max_length=32)
509
+
510
+ class ObservabilitySnapshotPayload(ObservabilityModel):
511
+ schema_version: Literal[1] = 1
512
+ snapshot_id: UUID
513
+ integration_type: Provider
514
+ evidence_type: Literal['mpulse_data', 'newrelic_data']
515
+ origin: ObservabilityOrigin
516
+ data_family: Identifier
517
+ result_type: ResultType
518
+ config_version: int = Field(ge=1, strict=True)
519
+ catalog_version: int = Field(ge=1, strict=True)
520
+ policy_version: int = Field(ge=1, strict=True)
521
+ source_signature: Sha256
522
+ recipe_dependencies: dict[Identifier, Sha256] = Field(min_length=1, max_length=50)
523
+ scope: ObservabilityFrozenScope
524
+ sources: list[ObservabilitySource] = Field(min_length=1, max_length=100)
525
+ parameters: ObservabilitySafeParameters
526
+ requested_windows: list[ObservabilityWindow] = Field(min_length=1, max_length=5)
527
+ actual_windows: list[ObservabilityCoverageWindow] = Field(default_factory=list, max_length=500)
528
+ columns: list[ObservabilityColumn] = Field(min_length=1, max_length=50)
529
+ data: list[DataItem] = Field(max_length=50000)
530
+ quality: ObservabilitySnapshotQuality
531
+ collected_at: AwareDatetime
532
+ expires_at: AwareDatetime
533
+
534
+ @model_validator(mode='after')
535
+ def validate_dataset(self) -> ObservabilitySnapshotPayload:
536
+ if self.evidence_type != f'{self.integration_type}_data':
537
+ raise ValueError('observability_provider_mismatch')
538
+ if not self.parameters.dataset_id.startswith(f'{self.integration_type}.'):
539
+ raise ValueError('observability_dataset_provider_mismatch')
540
+ if self.expires_at <= self.collected_at:
541
+ raise ValueError('observability_expiry_invalid')
542
+ source_refs = [source.target_ref for source in self.sources]
543
+ unique_values(source_refs)
544
+ unique_values([source.source_id for source in self.sources])
545
+ if set(source_refs) != set(self.scope.target_refs):
546
+ raise ValueError('observability_target_set_mismatch')
547
+ for source in self.sources:
548
+ unique_values(source.origin_scope_ids)
549
+ unique_values(source.market_codes)
550
+ for country in source.market_codes:
551
+ validate_country_code(country)
552
+ if self.integration_type == 'mpulse':
553
+ if not source.application_id or source.account_id is not None or source.entity_guid:
554
+ raise ValueError('observability_source_identity_invalid')
555
+ if source.endpoint_region != 'mpulse.soasta.com':
556
+ raise ValueError('observability_source_region_invalid')
557
+ elif source.application_id or source.account_id is None or (not source.entity_guid):
558
+ raise ValueError('observability_source_identity_invalid')
559
+ elif source.endpoint_region not in {'US', 'EU', 'JP'}:
560
+ raise ValueError('observability_source_region_invalid')
561
+ if source.completed and (not source.actual_windows):
562
+ raise ValueError('observability_completed_coverage_required')
563
+ if any((window.window_index >= len(self.requested_windows) for window in source.actual_windows)):
564
+ raise ValueError('observability_coverage_window_invalid')
565
+ if self.scope.scope_mode == 'common':
566
+ origins = {value for source in self.sources for value in source.origin_scope_ids}
567
+ if origins != set(self.scope.scope_ids) or any((source.market_codes for source in self.sources)):
568
+ raise ValueError('observability_scope_origins_mismatch')
569
+ else:
570
+ markets = {value for source in self.sources for value in source.market_codes}
571
+ if markets != set(self.scope.country_codes) or any((source.environment != self.scope.environment for source in self.sources)):
572
+ raise ValueError('observability_scope_origins_mismatch')
573
+ column_map = {column.field_id: column for column in self.columns}
574
+ unique_values([column.field_id for column in self.columns])
575
+ unique_values(self.parameters.field_ids)
576
+ if set(self.parameters.field_ids) != set(column_map):
577
+ raise ValueError('observability_projection_not_declared')
578
+ identifiers = []
579
+ point_count = 0
580
+ shape_kinds = {'summary': {'metric', 'row'}, 'table': {'row'}, 'records': {'row'}, 'timeseries': {'series', 'history'}, 'histogram': {'distribution'}, 'distribution': {'distribution'}, 'span': {'span'}, 'trace': {'span'}, 'metadata': {'metadata'}}
581
+ for item in self.data:
582
+ native_kind = item.original_kind if item.kind == 'redacted' else item.kind
583
+ if native_kind not in shape_kinds[self.result_type] or item.target_ref not in source_refs:
584
+ raise ValueError('observability_result_shape_or_target_invalid')
585
+ if item.window_index >= len(self.requested_windows):
586
+ raise ValueError('observability_data_window_invalid')
587
+ identifiers.append(item.evidence_id if item.kind == 'metric' else item.item_id)
588
+ if item.kind == 'redacted':
589
+ if not any((name in column_map and column_map[name].redaction != 'none' for name in item.values)):
590
+ raise ValueError('observability_redacted_representation_required')
591
+ if item.primary_field_id and (item.primary_field_id not in column_map or column_map[item.primary_field_id].redaction == 'none'):
592
+ raise ValueError('observability_redacted_representation_required')
593
+ identifiers.extend(item.bucket_refs)
594
+ point_count += len(item.bucket_refs)
595
+ if item.kind == 'history':
596
+ if item.field_id not in column_map:
597
+ raise ValueError('observability_projection_not_declared')
598
+ column = column_map[item.field_id]
599
+ if column.redaction != 'none':
600
+ raise ValueError('observability_redacted_representation_required')
601
+ if column.statistic != item.statistic or column.percentile != item.percentile:
602
+ raise ValueError('observability_statistic_mismatch')
603
+ point_count += max(1, len(item.values))
604
+ elif item.kind in {'series', 'distribution'}:
605
+ if item.field_id not in column_map:
606
+ raise ValueError('observability_projection_not_declared')
607
+ if column_map[item.field_id].redaction != 'none':
608
+ raise ValueError('observability_redacted_representation_required')
609
+ if item.kind == 'series':
610
+ column = column_map[item.field_id]
611
+ if column.statistic != item.statistic or column.percentile != item.percentile:
612
+ raise ValueError('observability_statistic_mismatch')
613
+ for field_id, value in item.dimensions.items():
614
+ if field_id not in column_map:
615
+ raise ValueError('observability_projection_not_declared')
616
+ validate_redacted_value(value, column_map[field_id].redaction)
617
+ expected = column_map[field_id].value_type
618
+ if value is not None and (not (expected == 'number' and type(value) in {int, float} or (expected == 'integer' and type(value) is int) or (expected == 'boolean' and type(value) is bool) or (expected in {'string', 'timestamp'} and isinstance(value, str)))):
619
+ raise ValueError('observability_column_type_invalid')
620
+ identifiers.extend((bucket.item_id for bucket in item.buckets))
621
+ point_count += len(item.buckets)
622
+ if self.result_type == 'histogram' and any((bucket.count is None or bucket.value is not None or bucket.native_value is not None for bucket in item.buckets)):
623
+ raise ValueError('observability_histogram_requires_native_counts')
624
+ elif item.kind == 'metric':
625
+ if item.metric_id not in column_map or item.integration_type != self.integration_type:
626
+ raise ValueError('observability_metric_not_declared')
627
+ if column_map[item.metric_id].redaction != 'none':
628
+ raise ValueError('observability_redacted_representation_required')
629
+ source = next((source for source in self.sources if source.target_ref == item.target_ref))
630
+ if item.source_id != source.source_id or item.data_family != self.data_family:
631
+ raise ValueError('observability_metric_source_mismatch')
632
+ if item.definition_version != column_map[item.metric_id].definition_version:
633
+ raise ValueError('observability_definition_version_mismatch')
634
+ if column_map[item.metric_id].statistic != item.statistic:
635
+ raise ValueError('observability_statistic_mismatch')
636
+ else:
637
+ if item.kind == 'span' and any((column.redaction != 'none' for column in self.columns)):
638
+ raise ValueError('observability_redacted_representation_required')
639
+ for field_id, value in item.values.items():
640
+ if field_id not in column_map:
641
+ raise ValueError('observability_projection_not_declared')
642
+ mode = column_map[field_id].redaction
643
+ validate_redacted_value(value, mode)
644
+ expected = column_map[field_id].value_type
645
+ valid = value is None or (expected == 'number' and type(value) in {int, float} or (expected == 'integer' and type(value) is int) or (expected == 'boolean' and type(value) is bool) or (expected in {'string', 'timestamp'} and isinstance(value, str)))
646
+ if not valid:
647
+ raise ValueError('observability_column_type_invalid')
648
+ if value is not None and expected == 'timestamp':
649
+ try:
650
+ if datetime.fromisoformat(value.replace('Z', '+00:00')).tzinfo is None:
651
+ raise ValueError
652
+ except ValueError:
653
+ raise ValueError('observability_timestamp_invalid') from None
654
+ unique_values(identifiers)
655
+ if point_count > 50000 or len(self.data) > 1000:
656
+ raise ValueError('observability_result_budget_exceeded')
657
+ if self.result_type == 'timeseries' and len(self.data) > 100:
658
+ raise ValueError('observability_series_budget_exceeded')
659
+ if self.result_type == 'trace' and len({item.trace_id for item in self.data if item.kind == 'span'}) > 5:
660
+ raise ValueError('observability_trace_budget_exceeded')
661
+ if self.result_type in {'span', 'trace'}:
662
+ if not {'span.id', 'trace.id', 'parent.id'} <= column_map.keys():
663
+ raise ValueError('observability_span_identifiers_required')
664
+ identities = [(item.target_ref, item.window_index, item.trace_id, item.span_id) for item in self.data if item.kind == 'span']
665
+ unique_values(identities)
666
+ for item in self.data:
667
+ if item.kind != 'span':
668
+ continue
669
+ missing = item.parent_span_id is not None and (item.target_ref, item.window_index, item.trace_id, item.parent_span_id) not in identities
670
+ if item.parent_missing != missing:
671
+ raise ValueError('observability_span_parent_coverage_invalid')
672
+ if self.quality.status == 'zero_matches' and (self.data or not self.quality.completed):
673
+ raise ValueError('observability_zero_matches_not_proven')
674
+ if self.quality.status == 'zero_matches' and any((source.status != 'zero_matches' or not source.completed for source in self.sources)):
675
+ raise ValueError('observability_zero_matches_not_proven')
676
+ if self.quality.status == 'available' and (not self.quality.completed or any((not source.completed or source.status not in {'available', 'zero_matches'} for source in self.sources))):
677
+ raise ValueError('observability_complete_target_coverage_required')
678
+ if self.quality.status == 'available' and (not self.data):
679
+ raise ValueError('observability_empty_result_not_explained')
680
+ return self
681
+
682
+ class ObservabilityAttachmentSelection(ObservabilityModel):
683
+ request_id: UUID
684
+ item_refs: list[Identifier] = Field(default_factory=list, max_length=1000)
685
+ field_ids: list[Identifier] = Field(default_factory=list, max_length=50)
686
+ representation_version: Literal[1, 2] = 1
687
+ include_zero_matches: bool = False
688
+
689
+ @model_validator(mode='after')
690
+ def validate_selection(self) -> ObservabilityAttachmentSelection:
691
+ unique_values(self.item_refs)
692
+ unique_values(self.field_ids)
693
+ if self.include_zero_matches:
694
+ if self.item_refs:
695
+ raise ValueError('observability_zero_matches_selection_invalid')
696
+ elif not self.item_refs or not self.field_ids:
697
+ raise ValueError('observability_attachment_selection_empty')
698
+ return self
699
+
700
+ class ObservabilityAttachmentPayload(ObservabilitySnapshotPayload):
701
+ attachment_id: UUID
702
+ parent_payload_sha256: Sha256
703
+ selection: ObservabilityAttachmentSelection
704
+ redaction_version: int = Field(ge=1, strict=True)
705
+
706
+ @model_validator(mode='after')
707
+ def validate_selected_content(self) -> ObservabilityAttachmentPayload:
708
+ if self.selection.field_ids != self.parameters.field_ids:
709
+ raise ValueError('observability_attachment_selection_mismatch')
710
+ if self.selection.representation_version != 2 and any((column.redaction != 'none' for column in self.columns)):
711
+ raise ValueError('observability_representation_version_required')
712
+ selected = set(self.selection.item_refs)
713
+ represented = set()
714
+ for item in self.data:
715
+ reference = item.evidence_id if item.kind == 'metric' else item.item_id
716
+ if reference in selected:
717
+ represented.add(reference)
718
+ elif item.kind == 'redacted' and item.bucket_refs:
719
+ references = set(item.bucket_refs)
720
+ if references - selected:
721
+ raise ValueError('observability_attachment_selection_mismatch')
722
+ represented.update(references)
723
+ elif item.kind in {'series', 'distribution'}:
724
+ references = {bucket.item_id for bucket in item.buckets}
725
+ if references - selected:
726
+ raise ValueError('observability_attachment_selection_mismatch')
727
+ represented.update(references)
728
+ else:
729
+ raise ValueError('observability_attachment_selection_mismatch')
730
+ if represented != selected:
731
+ raise ValueError('observability_attachment_selection_mismatch')
732
+ if self.selection.include_zero_matches != (self.quality.status == 'zero_matches'):
733
+ raise ValueError('observability_attachment_zero_matches_mismatch')
734
+ return self
735
+
736
+ class ObservabilityEvidenceReference(ObservabilityModel):
737
+ issuer: str = Field(min_length=1, max_length=2048)
738
+ workspace_id: UUID
739
+ project_id: UUID
740
+ user_id: UUID
741
+ snapshot_id: UUID
742
+ attachment_id: UUID
743
+ integration_type: Provider
744
+ evidence_type: Literal['mpulse_data', 'newrelic_data']
745
+ payload_sha256: Sha256
746
+ parent_payload_sha256: Sha256
747
+ _validate_issuer = field_validator('issuer')(validate_issuer)
748
+
749
+ @model_validator(mode='after')
750
+ def validate_provider(self) -> ObservabilityEvidenceReference:
751
+ if self.evidence_type != f'{self.integration_type}_data':
752
+ raise ValueError('observability_provider_mismatch')
753
+ return self
754
+
755
+ class ObservabilityAttachmentMetadata(ObservabilityEvidenceReference):
756
+ representation_version: Literal[1, 2] = 1
757
+ schema_version: Literal[1] = 1
758
+ byte_count: int = Field(ge=1, le=32768, strict=True)
759
+ expires_at: AwareDatetime
760
+ policy_version: int = Field(ge=1, strict=True)
761
+ current_policy_version: int = Field(ge=1, strict=True)
762
+ redaction_version: int = Field(ge=1, strict=True)
763
+ allow_agent_transfer: bool
764
+ allowed_model_providers: list[Identifier] = Field(default_factory=list, max_length=32)
765
+ consent_required: bool = False
766
+
767
+ @model_serializer(mode='wrap')
768
+ def serialize_metadata(self, handler):
769
+ value = handler(self)
770
+ if self.representation_version == 1:
771
+ value.pop('representation_version', None)
772
+ if not self.consent_required:
773
+ value.pop('consent_required', None)
774
+ return value
775
+ MAX_SNAPSHOT_BYTES = 10 * 1024 * 1024
776
+
777
+ def canonical_bytes(value: object) -> bytes:
778
+ try:
779
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')
780
+ except (ValueError, TypeError, UnicodeError, RecursionError):
781
+ raise ValueError('observability_invalid_json') from None
782
+
783
+ def _unique_object(pairs):
784
+ result = {}
785
+ for key, value in pairs:
786
+ if key in result:
787
+ raise ValueError('observability_invalid_json')
788
+ result[key] = value
789
+ return result
790
+
791
+ def _invalid_constant(_value):
792
+ raise ValueError('observability_invalid_json')
793
+
794
+ def strict_json_loads(payload: bytes, *, max_bytes: int=MAX_SNAPSHOT_BYTES):
795
+ if not isinstance(payload, bytes) or not payload or len(payload) > max_bytes:
796
+ raise ValueError('observability_payload_size_invalid')
797
+ try:
798
+ value = json.loads(payload.decode('utf-8'), object_pairs_hook=_unique_object, parse_constant=_invalid_constant)
799
+ canonical_bytes(value)
800
+ return value
801
+ except (ValueError, TypeError, UnicodeError, RecursionError):
802
+ raise ValueError('observability_invalid_json') from None
803
+ ObservabilityModel.model_rebuild()
804
+ ObservabilityWindow.model_rebuild()
805
+ ObservabilitySelection.model_rebuild()
806
+ ObservabilityCondition.model_rebuild()
807
+ ObservabilityFilterGroup.model_rebuild()
808
+ ObservabilityOrder.model_rebuild()
809
+ ObservabilityQuery.model_rebuild()
810
+ ObservabilityOrigin.model_rebuild()
811
+ ObservabilityCoverageWindow.model_rebuild()
812
+ ObservabilitySource.model_rebuild()
813
+ ObservabilitySafeCondition.model_rebuild()
814
+ ObservabilitySafeFilterGroup.model_rebuild()
815
+ ObservabilityFrozenScope.model_rebuild()
816
+ ObservabilitySafeParameters.model_rebuild()
817
+ ObservabilityColumn.model_rebuild()
818
+ ObservabilityMetricQuality.model_rebuild()
819
+ ObservabilityMetricValue.model_rebuild()
820
+ ObservabilityRow.model_rebuild()
821
+ ObservabilityTimeBucket.model_rebuild()
822
+ ObservabilitySeries.model_rebuild()
823
+ ObservabilityHistory.model_rebuild()
824
+ ObservabilityDistributionBucket.model_rebuild()
825
+ ObservabilityDistribution.model_rebuild()
826
+ ObservabilitySpan.model_rebuild()
827
+ ObservabilityRedactedItem.model_rebuild()
828
+ ObservabilitySnapshotQuality.model_rebuild()
829
+ ObservabilitySnapshotPayload.model_rebuild()
830
+ ObservabilityAttachmentSelection.model_rebuild()
831
+ ObservabilityAttachmentPayload.model_rebuild()
832
+ ObservabilityEvidenceReference.model_rebuild()
833
+ ObservabilityAttachmentMetadata.model_rebuild()
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from functools import lru_cache
5
+ from importlib import import_module
6
+
7
+ EVIDENCE_PROTOCOL_VERSION = 1
8
+ PAYLOAD_MAX_BYTES = 32 * 1024
9
+ TURN_MAX_BYTES = 64 * 1024
10
+ ORIGIN_FIELDS = frozenset({"issuer", "workspace_id", "project_id", "user_id"})
11
+
12
+
13
+ class EvidenceError(ValueError):
14
+ pass
15
+
16
+
17
+ @lru_cache(maxsize=1)
18
+ def _contract():
19
+ try:
20
+ if __package__ == "app":
21
+ module = import_module("app.schemas.observability_evidence")
22
+ serialization = import_module("app.observability_lifecycle")
23
+ else:
24
+ module = import_module(
25
+ f"{__package__}.observability_contract" if __package__ else "observability_contract"
26
+ )
27
+ serialization = module
28
+ except ImportError:
29
+ raise EvidenceError("observability_evidence_protocol_unsupported") from None
30
+ return (
31
+ module.ObservabilityEvidenceReference, module.ObservabilityAttachmentMetadata,
32
+ module.ObservabilityAttachmentPayload,
33
+ serialization.canonical_bytes, serialization.strict_json_loads,
34
+ )
35
+
36
+
37
+ def validate_reference(reference) -> dict:
38
+ reference_model, _metadata, _payload, canonical, _loads = _contract()
39
+ try:
40
+ parsed = reference_model.model_validate_json(canonical(reference), strict=True)
41
+ normalized = parsed.model_dump(mode="json")
42
+ if normalized != reference:
43
+ raise ValueError
44
+ except (TypeError, ValueError):
45
+ raise EvidenceError("observability_evidence_invalid_reference") from None
46
+ return normalized
47
+
48
+
49
+ def validate_payload(
50
+ reference, metadata, payload, *, expected_origin, model_provider,
51
+ trusted_transport=False, now=None,
52
+ ) -> dict:
53
+ import hashlib
54
+
55
+ if trusted_transport is not True:
56
+ raise EvidenceError("observability_evidence_trusted_transport_required")
57
+ reference = validate_reference(reference)
58
+ if (
59
+ not isinstance(expected_origin, dict) or set(expected_origin) != ORIGIN_FIELDS
60
+ or any(reference[key] != expected_origin[key] for key in ORIGIN_FIELDS)
61
+ ):
62
+ raise EvidenceError("observability_evidence_identity_mismatch")
63
+ if not isinstance(payload, str):
64
+ raise EvidenceError("observability_evidence_invalid_payload")
65
+ try:
66
+ encoded = payload.encode("utf-8")
67
+ except UnicodeError:
68
+ raise EvidenceError("observability_evidence_invalid_payload") from None
69
+ if not 1 <= len(encoded) <= PAYLOAD_MAX_BYTES:
70
+ raise EvidenceError("observability_evidence_size_limit")
71
+ if hashlib.sha256(encoded).hexdigest() != reference["payload_sha256"]:
72
+ raise EvidenceError("observability_evidence_integrity_failed")
73
+ _reference, metadata_model, payload_model, canonical, loads = _contract()
74
+ try:
75
+ current = metadata_model.model_validate_json(canonical(metadata), strict=True)
76
+ parsed = loads(encoded)
77
+ body = payload_model.model_validate_json(canonical(parsed), strict=True)
78
+ if canonical(body.model_dump(mode="json")) != encoded:
79
+ raise ValueError
80
+ except (TypeError, ValueError):
81
+ raise EvidenceError("observability_evidence_invalid_payload") from None
82
+ current_fields = current.model_dump(mode="json")
83
+ if any(current_fields[key] != value for key, value in reference.items()):
84
+ raise EvidenceError("observability_evidence_identity_mismatch")
85
+ if (
86
+ str(body.snapshot_id) != reference["snapshot_id"]
87
+ or str(body.attachment_id) != reference["attachment_id"]
88
+ or body.integration_type != reference["integration_type"]
89
+ or body.evidence_type != reference["evidence_type"]
90
+ or body.parent_payload_sha256 != reference["parent_payload_sha256"]
91
+ or body.origin.issuer != reference["issuer"]
92
+ or str(body.origin.workspace_id) != reference["workspace_id"]
93
+ or str(body.origin.project_id) != reference["project_id"]
94
+ or str(body.origin.created_by_user_id) != reference["user_id"]
95
+ ):
96
+ raise EvidenceError("observability_evidence_identity_mismatch")
97
+ if (
98
+ current.byte_count != len(encoded)
99
+ or current.representation_version != body.selection.representation_version
100
+ or current.redaction_version != body.redaction_version
101
+ or current.policy_version != body.policy_version
102
+ or current.current_policy_version < current.policy_version
103
+ ):
104
+ raise EvidenceError("observability_evidence_integrity_failed")
105
+ timestamp = now or datetime.now(timezone.utc)
106
+ if current.expires_at <= timestamp or body.expires_at < current.expires_at:
107
+ raise EvidenceError("observability_evidence_expired")
108
+ if (
109
+ current.allow_agent_transfer is not True
110
+ or model_provider not in current.allowed_model_providers
111
+ ):
112
+ raise EvidenceError("observability_evidence_policy_denied")
113
+ return {
114
+ "reference": reference, "payload_json": payload, "byte_count": len(encoded),
115
+ "policy_version": current.current_policy_version,
116
+ "redaction_version": current.redaction_version,
117
+ "representation_version": current.representation_version,
118
+ "expires_at": current.expires_at.isoformat(),
119
+ "model_provider": model_provider, "data_family": body.data_family,
120
+ "result_type": body.result_type, "field_ids": body.parameters.field_ids,
121
+ }
122
+
123
+
124
+ def validate_turn_budget(payloads: list[bytes]) -> int:
125
+ total = 0
126
+ for payload in payloads:
127
+ if not isinstance(payload, bytes) or not 1 <= len(payload) <= PAYLOAD_MAX_BYTES:
128
+ raise EvidenceError("observability_evidence_size_limit")
129
+ total += len(payload)
130
+ if total > TURN_MAX_BYTES:
131
+ raise EvidenceError("observability_turn_budget_exceeded")
132
+ return total
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.78.0
3
+ Version: 1.78.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -26,6 +26,8 @@ Requires-Python: >=3.9
26
26
  Description-Content-Type: text/markdown
27
27
  Requires-Dist: httpx>=0.24
28
28
  Requires-Dist: socksio>=1.0.0
29
+ Requires-Dist: pydantic<3,>=2.10
30
+ Requires-Dist: pycountry>=24.6
29
31
  Provides-Extra: daemon
30
32
  Requires-Dist: httpx>=0.24; extra == "daemon"
31
33
  Requires-Dist: socksio>=1.0.0; extra == "daemon"
@@ -9,6 +9,8 @@ forgexa_cli/aws_evidence.py
9
9
  forgexa_cli/aws_logs_evidence.py
10
10
  forgexa_cli/daemon.py
11
11
  forgexa_cli/main.py
12
+ forgexa_cli/observability_contract.py
13
+ forgexa_cli/observability_evidence.py
12
14
  forgexa_cli/py.typed
13
15
  forgexa_cli/runtime_evidence.py
14
16
  forgexa_cli/zcode_protocol.py
@@ -1,5 +1,7 @@
1
1
  httpx>=0.24
2
2
  socksio>=1.0.0
3
+ pydantic<3,>=2.10
4
+ pycountry>=24.6
3
5
 
4
6
  [daemon]
5
7
  httpx>=0.24
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.78.0"
3
+ version = "1.78.2"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
@@ -28,7 +28,7 @@ classifiers = [
28
28
  # installs it automatically — no fragile runtime auto-install needed.
29
29
  # socksio is required for SOCKS proxy support (httpx picks up ALL_PROXY/SOCKS_PROXY env vars
30
30
  # automatically, and raises ImportError if socksio is missing).
31
- dependencies = ["httpx>=0.24", "socksio>=1.0.0"]
31
+ dependencies = ["httpx>=0.24", "socksio>=1.0.0", "pydantic>=2.10,<3", "pycountry>=24.6"]
32
32
 
33
33
  [project.optional-dependencies]
34
34
  # Keep [daemon] extra for backward compatibility / explicit opt-in.
File without changes
File without changes