f-layer 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. f_layer-2.0.0.dist-info/METADATA +232 -0
  2. f_layer-2.0.0.dist-info/RECORD +45 -0
  3. f_layer-2.0.0.dist-info/WHEEL +4 -0
  4. f_layer-2.0.0.dist-info/entry_points.txt +2 -0
  5. f_layer-2.0.0.dist-info/licenses/LICENSE +201 -0
  6. flayer/__init__.py +5 -0
  7. flayer/__main__.py +247 -0
  8. flayer/_vpn_guest.py +130 -0
  9. flayer/core/__init__.py +1 -0
  10. flayer/core/config.py +198 -0
  11. flayer/core/contracts.py +114 -0
  12. flayer/core/lifecycle.py +767 -0
  13. flayer/core/state.py +351 -0
  14. flayer/diagnostics/__init__.py +20 -0
  15. flayer/diagnostics/benchmark.py +272 -0
  16. flayer/diagnostics/contracts.py +167 -0
  17. flayer/diagnostics/health.py +154 -0
  18. flayer/diagnostics/reporting.py +24 -0
  19. flayer/extensions/__init__.py +25 -0
  20. flayer/extensions/builtins.py +165 -0
  21. flayer/extensions/contracts.py +196 -0
  22. flayer/extensions/discovery.py +152 -0
  23. flayer/extensions/registry.py +347 -0
  24. flayer/guest.py +270 -0
  25. flayer/integrations/__init__.py +23 -0
  26. flayer/integrations/adapters.py +92 -0
  27. flayer/integrations/contracts.py +241 -0
  28. flayer/integrations/wire.py +273 -0
  29. flayer/profiles/__init__.py +33 -0
  30. flayer/profiles/_amnezia_export.py +176 -0
  31. flayer/profiles/_vless_guest.py +432 -0
  32. flayer/profiles/amneziawg.py +555 -0
  33. flayer/profiles/artifacts.py +609 -0
  34. flayer/profiles/gateway.py +729 -0
  35. flayer/profiles/static_site.py +596 -0
  36. flayer/profiles/vless.py +422 -0
  37. flayer/profiles/vpn.py +524 -0
  38. flayer/providers/__init__.py +1 -0
  39. flayer/providers/contracts.py +207 -0
  40. flayer/providers/lifecycle.py +177 -0
  41. flayer/providers/yandex.py +387 -0
  42. flayer/providers/yandex_lifecycle.py +781 -0
  43. flayer/py.typed +0 -0
  44. flayer/vpn_export.py +72 -0
  45. flayer/vpn_project.py +828 -0
flayer/guest.py ADDED
@@ -0,0 +1,270 @@
1
+ """Strict host-pinned SSH transport for private VPN installation and service observations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import os
8
+ import re
9
+ import shlex
10
+ import stat
11
+ import subprocess
12
+ from dataclasses import dataclass, field
13
+ from importlib.resources import files
14
+ from pathlib import Path
15
+ from typing import Protocol
16
+
17
+ from flayer.profiles.artifacts import ArtifactBundle
18
+ from flayer.profiles.vpn import VpnEndpoint, VpnError
19
+
20
+ MAX_GUEST_REQUEST_BYTES = 4 * 1024 * 1024
21
+ _SERVICE = re.compile(r"[a-z][a-z0-9-]{0,79}\.service\Z")
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class GuestResult:
26
+ """Transient subprocess output excluded from object representations and error messages."""
27
+
28
+ return_code: int
29
+ stdout: bytes = field(default=b"", repr=False)
30
+ stderr: bytes = field(default=b"", repr=False)
31
+
32
+
33
+ class GuestRunner(Protocol):
34
+ """Injectable subprocess boundary for deterministic tests without SSH connections."""
35
+
36
+ def Run(self, command: tuple[str, ...], payload: bytes, timeout: float) -> GuestResult:
37
+ """Run a bounded argument vector with private standard input and captured output."""
38
+
39
+ ...
40
+
41
+
42
+ class SubprocessGuestRunner:
43
+ """Execute OpenSSH without a local shell or inherited standard input."""
44
+
45
+ def Run(self, command: tuple[str, ...], payload: bytes, timeout: float) -> GuestResult:
46
+ """Keep any guest or SSH diagnostic text out of user-visible exception messages."""
47
+
48
+ try:
49
+ result = subprocess.run(
50
+ command, input=payload, capture_output=True, check=False, shell=False, timeout=timeout,
51
+ )
52
+
53
+ except (OSError, subprocess.TimeoutExpired):
54
+ raise VpnError("Guest connection failed or exceeded its time limit") from None
55
+
56
+ return GuestResult(result.returncode, result.stdout, result.stderr)
57
+
58
+
59
+ def ValidateSshPathSyntax(path: Path) -> None:
60
+ """Reject OpenSSH substitutions before authorizing any project or guest operation."""
61
+
62
+ if any(character in str(path) for character in ('"', "\n", "\r", "\\", "%", "$")):
63
+ raise VpnError("Project paths must not contain quotes, controls, backslashes, percent signs, or dollar signs")
64
+
65
+
66
+ def PrivatePath(path: Path, *, directory: bool = False) -> Path:
67
+ """Require caller-owned private POSIX objects without symlinks or hardlinked files."""
68
+
69
+ if os.name != "posix" or ".." in path.parts:
70
+ raise VpnError("VPN projects require a POSIX host and paths without traversal")
71
+
72
+ absolute = path.absolute()
73
+
74
+ if any(item.is_symlink() for item in (absolute, *absolute.parents)):
75
+ raise VpnError("VPN project paths must not contain symbolic links")
76
+
77
+ try:
78
+ info = absolute.lstat()
79
+
80
+ except OSError:
81
+ raise VpnError("Required private project path is unavailable") from None
82
+
83
+ kind = stat.S_ISDIR if directory else stat.S_ISREG
84
+
85
+ if (
86
+ not kind(info.st_mode) or info.st_uid != os.getuid()
87
+ or stat.S_IMODE(info.st_mode) != (0o700 if directory else 0o600)
88
+ or (not directory and info.st_nlink != 1)
89
+ ):
90
+ raise VpnError("VPN project paths must be caller-owned directories0700 or files0600")
91
+
92
+ return absolute
93
+
94
+
95
+ def ReadPrivate(path: Path, *, limit: int = 65536) -> bytes:
96
+ """Read one bounded private file and detect replacements or in-place edits during the read."""
97
+
98
+ absolute = PrivatePath(path)
99
+ descriptor: int | None = None
100
+
101
+ try:
102
+ before = absolute.lstat()
103
+ descriptor = os.open(absolute, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
104
+ opened = os.fstat(descriptor)
105
+
106
+ if (before.st_dev, before.st_ino) != (opened.st_dev, opened.st_ino) or not stat.S_ISREG(opened.st_mode):
107
+ raise VpnError("Private project file changed during read")
108
+
109
+ if opened.st_uid != os.getuid() or stat.S_IMODE(opened.st_mode) != 0o600 or opened.st_nlink != 1:
110
+ raise VpnError("Private project file ownership changed during read")
111
+
112
+ content = bytearray()
113
+
114
+ while len(content) <= limit:
115
+ chunk = os.read(descriptor, min(65536, limit + 1 - len(content)))
116
+
117
+ if not chunk:
118
+ break
119
+
120
+ content.extend(chunk)
121
+
122
+ after = os.fstat(descriptor)
123
+
124
+ if (
125
+ len(content) > limit or opened.st_size != after.st_size
126
+ or opened.st_mtime_ns != after.st_mtime_ns or opened.st_ctime_ns != after.st_ctime_ns
127
+ ):
128
+ raise VpnError("Private project file is oversized or changed during read")
129
+
130
+ return bytes(content)
131
+
132
+ except OSError:
133
+ raise VpnError("Unable to read a private project file") from None
134
+
135
+ finally:
136
+ if descriptor is not None:
137
+ os.close(descriptor)
138
+
139
+
140
+ def WritePrivate(path: Path, content: bytes) -> None:
141
+ """Create a private project file exclusively without overwriting an existing object."""
142
+
143
+ PrivatePath(path.parent, directory=True)
144
+
145
+ try:
146
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
147
+
148
+ with os.fdopen(descriptor, "wb") as stream:
149
+ stream.write(content)
150
+ stream.flush()
151
+ os.fsync(stream.fileno())
152
+
153
+ except OSError:
154
+ raise VpnError("Unable to exclusively create a private project file") from None
155
+
156
+
157
+ class SshGuestProvisioner:
158
+ """Connect only to an explicit IPv4 endpoint using an independently pinned Ed25519 host key."""
159
+
160
+ def __init__(
161
+ self, endpoint: VpnEndpoint, username: str, identity_file: Path,
162
+ known_hosts_file: Path, runner: GuestRunner | None = None,
163
+ ) -> None:
164
+ """Validate private key/trust paths before constructing any SSH argument vector."""
165
+
166
+ import ipaddress
167
+
168
+ try:
169
+ ipaddress.IPv4Address(endpoint.host)
170
+
171
+ except (AttributeError, ValueError):
172
+ raise VpnError("Guest provisioning requires the actual reserved IPv4 address") from None
173
+
174
+ if not isinstance(username, str) or re.fullmatch(r"[a-z][a-z0-9-]{0,31}", username) is None or username == "root":
175
+ raise VpnError("Guest administration requires an explicit non-root username")
176
+
177
+ identity_file = PrivatePath(identity_file)
178
+ known_hosts_file = PrivatePath(known_hosts_file)
179
+
180
+ ValidateSshPathSyntax(identity_file)
181
+ ValidateSshPathSyntax(known_hosts_file)
182
+
183
+ if not ReadPrivate(known_hosts_file).strip():
184
+ raise VpnError("Guest provisioning requires an independently authenticated host-key pin")
185
+
186
+ self.runner = runner or SubprocessGuestRunner()
187
+ self.command = (
188
+ "ssh", "-F", "/dev/null", "-T", "-p", str(endpoint.port), "-i", str(identity_file),
189
+ "-o", "BatchMode=yes", "-o", "IdentityAgent=none", "-o", "IdentitiesOnly=yes",
190
+ "-o", "StrictHostKeyChecking=yes", "-o", "GlobalKnownHostsFile=/dev/null",
191
+ "-o", f'UserKnownHostsFile="{known_hosts_file}"', "-o", "UpdateHostKeys=no",
192
+ "-o", "VerifyHostKeyDNS=no", "-o", "HostKeyAlgorithms=ssh-ed25519",
193
+ "-o", "ConnectTimeout=15", "-o", "ConnectionAttempts=1",
194
+ "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3",
195
+ "-o", "ControlMaster=no", "-o", "ClearAllForwardings=yes",
196
+ "-o", "PermitLocalCommand=no", "-o", "ProxyCommand=none", "-o", "ProxyJump=none",
197
+ f"{username}@{endpoint.host}",
198
+ )
199
+
200
+ def Install(self, bundles: tuple[tuple[str, ArtifactBundle], ...]) -> None:
201
+ """Stage only validated sensitive server files on the trusted guest and run installers."""
202
+
203
+ if not isinstance(bundles, tuple) or not 1 <= len(bundles) <= 2 or any(
204
+ not isinstance(item, tuple) or len(item) != 2 for item in bundles
205
+ ):
206
+ raise VpnError("Guest installation requires one or two explicit server bundles")
207
+
208
+ transports: set[str] = set()
209
+ documents = []
210
+
211
+ for transport, bundle in bundles:
212
+ if (
213
+ not isinstance(transport, str) or transport not in {"amneziawg", "vless-reality"} or transport in transports
214
+ or not isinstance(bundle, ArtifactBundle) or bundle.kind != "server"
215
+ or any(not item.sensitive for item in bundle.files)
216
+ ):
217
+ raise VpnError("Guest installation requires unique supported private server bundles")
218
+
219
+ transports.add(transport)
220
+ documents.append({"transport": transport, "files": {
221
+ item.name: base64.b64encode(item.content).decode("ascii") for item in bundle.files
222
+ }})
223
+
224
+ if len({bundle.identity for _, bundle in bundles}) != 1:
225
+ raise VpnError("Guest installation cannot mix different stack owners")
226
+
227
+ payload = json.dumps({"bundles": documents}, sort_keys=True).encode("utf-8")
228
+
229
+ if len(payload) > MAX_GUEST_REQUEST_BYTES:
230
+ raise VpnError("Guest installation request exceeds its bounded size")
231
+
232
+ command = "sudo -n /usr/bin/python3 -c " + shlex.quote(files("flayer").joinpath("_vpn_guest.py").read_text(encoding="utf-8"))
233
+ result = self.runner.Run((*self.command, command), payload, 3700)
234
+
235
+ if result.return_code != 0 or result.stdout.strip() != b"FLAYER_GUEST_INSTALL_OK":
236
+ receipt = result.stdout.strip() if len(result.stdout) <= 128 else b""
237
+ match = re.fullmatch(rb"FLAYER_GUEST_FAILED (amneziawg|vless-reality) (stage|install|check)", receipt)
238
+
239
+ if match is not None:
240
+ transport, phase = (item.decode("ascii") for item in match.groups())
241
+ hint = (
242
+ "Check the Reality target supports TLS 1.3 and HTTP/2, and guest access to the pinned Xray download"
243
+ if transport == "vless-reality" else
244
+ "Check Ubuntu 24.04 amd64 compatibility and guest access to the pinned Go and GitHub downloads"
245
+ )
246
+
247
+ raise VpnError(f"VPN {transport} {phase} failed. {hint}. Cloud resources and private artifacts were retained for retry")
248
+
249
+ raise VpnError("Guest SSH connection or installation failed. Check administrator source CIDRs, host trust, and guest logs; resources and private artifacts were retained for retry")
250
+
251
+ def ServiceStatus(self, services: tuple[str, ...]) -> tuple[tuple[str, bool], ...]:
252
+ """Observe service activity without claiming client connectivity or performing guest writes."""
253
+
254
+ if not isinstance(services, tuple) or not 1 <= len(services) <= 2 or any(
255
+ not isinstance(item, str) or _SERVICE.fullmatch(item) is None for item in services
256
+ ):
257
+ raise VpnError("Guest status requires bounded explicit service names")
258
+
259
+ result = []
260
+
261
+ for service in services:
262
+ command = "sudo -n /usr/bin/systemctl is-active --quiet " + shlex.quote(service)
263
+ observed = self.runner.Run((*self.command, command), b"", 30)
264
+
265
+ if observed.return_code not in {0, 3}:
266
+ raise VpnError("Unable to observe the owned guest service")
267
+
268
+ result.append((service, observed.return_code == 0))
269
+
270
+ return tuple(result)
@@ -0,0 +1,23 @@
1
+ """Consumer-neutral observation contracts without transport, dispatch, or authorization."""
2
+
3
+ from .adapters import AdaptDiagnostics, AdaptLifecycle, AdaptState
4
+ from .contracts import (
5
+ CheckEvidence,
6
+ DiagnosticEvidence,
7
+ IntegrationError,
8
+ IntegrationOperation,
9
+ IntegrationRequest,
10
+ IntegrationResult,
11
+ LifecycleEvidence,
12
+ Metric,
13
+ ResourceCount,
14
+ StateEvidence,
15
+ )
16
+ from .wire import ParseRequest, ParseResult, SerializeRequest, SerializeResult
17
+
18
+ __all__ = [
19
+ "AdaptDiagnostics", "AdaptLifecycle", "AdaptState", "CheckEvidence", "DiagnosticEvidence",
20
+ "IntegrationError", "IntegrationOperation", "IntegrationRequest", "IntegrationResult",
21
+ "LifecycleEvidence", "Metric", "ParseRequest", "ParseResult", "ResourceCount", "SerializeRequest",
22
+ "SerializeResult", "StateEvidence",
23
+ ]
@@ -0,0 +1,92 @@
1
+ """Read-only conversions project trusted caller-selected models into minimal evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from typing import cast
7
+
8
+ from flayer.core.contracts import StackIdentity
9
+ from flayer.core.lifecycle import LifecycleObservation, LifecycleReport
10
+ from flayer.core.state import StackState
11
+ from flayer.diagnostics.contracts import DiagnosticReport
12
+
13
+ from .contracts import (
14
+ MAX_CHECKS,
15
+ MAX_RESOURCES,
16
+ METRIC_BOUNDS,
17
+ RESOURCE_STATUSES,
18
+ CheckEvidence,
19
+ DiagnosticEvidence,
20
+ IntegrationError,
21
+ IntegrationOperation,
22
+ IntegrationRequest,
23
+ IntegrationResult,
24
+ LifecycleEvidence,
25
+ Metric,
26
+ RequireIdentity,
27
+ ResourceCount,
28
+ StateEvidence,
29
+ )
30
+
31
+
32
+ def AdaptDiagnostics(
33
+ request: IntegrationRequest, report: DiagnosticReport, observed_identity: StackIdentity,
34
+ ) -> IntegrationResult:
35
+ """Bind selected status, health, or benchmark observations and omit arbitrary source text."""
36
+
37
+ RequireIdentity(observed_identity, request.identity)
38
+
39
+ if (
40
+ request.operation not in {
41
+ IntegrationOperation.STATUS, IntegrationOperation.HEALTH, IntegrationOperation.BENCHMARK,
42
+ }
43
+ or not isinstance(report, DiagnosticReport)
44
+ or report.command != request.operation.value
45
+ or len(report.checks) > MAX_CHECKS
46
+ ):
47
+ raise IntegrationError("Diagnostic source does not match the integration request")
48
+
49
+ checks = tuple(CheckEvidence(check.status, tuple(
50
+ Metric(name, cast(int | float | None, check.details[name])) for name in sorted(METRIC_BOUNDS)
51
+ if name in check.details
52
+ )) for check in report.checks)
53
+
54
+ return IntegrationResult(request, DiagnosticEvidence(checks))
55
+
56
+
57
+ def AdaptState(request: IntegrationRequest, state: StackState) -> IntegrationResult:
58
+ """Summarize recorded ownership without exporting locators or claiming remote readiness."""
59
+
60
+ if request.operation is not IntegrationOperation.STATE or not isinstance(state, StackState):
61
+ raise IntegrationError("Owned state source does not match the integration request")
62
+
63
+ RequireIdentity(state.identity, request.identity)
64
+
65
+ return IntegrationResult(request, StateEvidence(len(state.resources)))
66
+
67
+
68
+ def AdaptLifecycle(
69
+ request: IntegrationRequest, report: LifecycleReport, observed_identity: StackIdentity,
70
+ ) -> IntegrationResult:
71
+ """Summarize supplied lifecycle evidence with caller-bound scope, never remote attestation."""
72
+
73
+ RequireIdentity(observed_identity, request.identity)
74
+
75
+ if (
76
+ request.operation is not IntegrationOperation.LIFECYCLE_REPORT
77
+ or not isinstance(report, LifecycleReport)
78
+ or not isinstance(report.resources, tuple)
79
+ or len(report.resources) > MAX_RESOURCES
80
+ or any(not isinstance(resource, LifecycleObservation) for resource in report.resources)
81
+ or any(not isinstance(resource.status, str) or resource.status not in RESOURCE_STATUSES
82
+ for resource in report.resources)
83
+ ):
84
+ raise IntegrationError("Lifecycle source does not match the integration request")
85
+
86
+ counts = Counter(resource.status for resource in report.resources)
87
+ evidence = LifecycleEvidence(
88
+ report.action, report.status, report.recovery_required,
89
+ tuple(ResourceCount(status, count) for status, count in sorted(counts.items())),
90
+ )
91
+
92
+ return IntegrationResult(request, evidence)
@@ -0,0 +1,241 @@
1
+ """Consumer-neutral immutable envelopes for strictly allowlisted integration evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+ from typing import TypeAlias
10
+
11
+ from flayer.core.contracts import ContractError, StackIdentity, ValidateSchemaVersion
12
+ from flayer.diagnostics.contracts import STATUS_PRIORITY, DiagnosticStatus
13
+
14
+ MAX_WIRE_BYTES = 64 * 1024
15
+ MAX_CHECKS = 128
16
+ MAX_RESOURCES = 4096
17
+ REQUEST_ID_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,63}\Z")
18
+ METRIC_BOUNDS = {
19
+ "duration_seconds": (0, 30),
20
+ "received_bytes": (0, 1024 * 1024),
21
+ "byte_limit": (1, 1024 * 1024),
22
+ "http_status": (100, 599),
23
+ "throughput_mbps": (0, 1_000_000_000_000),
24
+ }
25
+ INTEGER_METRICS = frozenset({"received_bytes", "byte_limit", "http_status"})
26
+ NULLABLE_METRICS = frozenset({"http_status", "throughput_mbps"})
27
+ LIFECYCLE_ACTIONS = frozenset({"status", "create", "destroy", "recover"})
28
+ LIFECYCLE_STATUSES = frozenset({
29
+ "complete", "incomplete", "uncertain", "rolled-back", "rollback-incomplete",
30
+ })
31
+ RESOURCE_STATUSES = frozenset({"recorded", "present", "missing", "not-recorded"})
32
+
33
+
34
+ class IntegrationError(ContractError):
35
+ """An envelope violates the bounded evidence contract without exposing its input."""
36
+
37
+
38
+ class IntegrationOperation(StrEnum):
39
+ """Supported observations convey no infrastructure execution authorization."""
40
+
41
+ STATUS = "status"
42
+ HEALTH = "health"
43
+ BENCHMARK = "benchmark"
44
+ STATE = "state"
45
+ LIFECYCLE_REPORT = "lifecycle-report"
46
+
47
+
48
+ def RequireIdentity(actual: StackIdentity, expected: StackIdentity) -> None:
49
+ """Require all ownership and provider scope fields to match independently held context."""
50
+
51
+ if not isinstance(expected, StackIdentity) or actual != expected:
52
+ raise IntegrationError("Integration identity does not match the expected ownership boundary")
53
+
54
+
55
+ def ValidateCount(value: object) -> int:
56
+ """Reject coercion and bound recorded-resource and observation counts."""
57
+
58
+ if type(value) is not int or not 0 <= value <= MAX_RESOURCES:
59
+ raise IntegrationError("Integration resource count exceeds its supported bounds")
60
+
61
+ return value
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class IntegrationRequest:
66
+ """Non-authorizing observation intent bound to exact scope, owner, and correlation."""
67
+
68
+ identity: StackIdentity
69
+ request_id: str
70
+ operation: IntegrationOperation
71
+ schema_version: int = 1
72
+
73
+ def __post_init__(self) -> None:
74
+ """Validate direct requests with the same strict rules as decoded requests."""
75
+
76
+ ValidateSchemaVersion(self.schema_version)
77
+
78
+ if (
79
+ not isinstance(self.identity, StackIdentity)
80
+ or not isinstance(self.request_id, str)
81
+ or REQUEST_ID_PATTERN.fullmatch(self.request_id) is None
82
+ or not isinstance(self.operation, IntegrationOperation)
83
+ ):
84
+ raise IntegrationError("Integration request fields do not match the contract")
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class Metric:
89
+ """One allowlisted bounded measurement that cannot contain strings or raw payloads."""
90
+
91
+ name: str
92
+ value: int | float | None
93
+
94
+ def __post_init__(self) -> None:
95
+ """Require finite measurements, strict integer metrics, and supported nullable fields."""
96
+
97
+ if not isinstance(self.name, str) or self.name not in METRIC_BOUNDS:
98
+ raise IntegrationError("Integration metric is unsupported")
99
+
100
+ if self.value is None:
101
+ if self.name not in NULLABLE_METRICS:
102
+ raise IntegrationError("Integration metric cannot be null")
103
+
104
+ return
105
+
106
+ lower, upper = METRIC_BOUNDS[self.name]
107
+
108
+ if (
109
+ type(self.value) not in {int, float}
110
+ or (self.name in INTEGER_METRICS and type(self.value) is not int)
111
+ or not lower <= self.value <= upper
112
+ or not math.isfinite(self.value)
113
+ ):
114
+ raise IntegrationError("Integration metric exceeds its supported bounds")
115
+
116
+
117
+ @dataclass(frozen=True, slots=True)
118
+ class CheckEvidence:
119
+ """A diagnostic outcome and measurements without private names, prose, or details."""
120
+
121
+ status: DiagnosticStatus
122
+ metrics: tuple[Metric, ...] = ()
123
+
124
+ def __post_init__(self) -> None:
125
+ """Reject mutable or duplicate measurements and invalid diagnostic outcomes."""
126
+
127
+ if (
128
+ not isinstance(self.status, DiagnosticStatus)
129
+ or not isinstance(self.metrics, tuple)
130
+ or len(self.metrics) > len(METRIC_BOUNDS)
131
+ or any(not isinstance(metric, Metric) for metric in self.metrics)
132
+ or len({metric.name for metric in self.metrics}) != len(self.metrics)
133
+ ):
134
+ raise IntegrationError("Integration check fields do not match the contract")
135
+
136
+ object.__setattr__(self, "metrics", tuple(sorted(self.metrics, key=lambda metric: metric.name)))
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class DiagnosticEvidence:
141
+ """Ordered diagnostic checks retain truthful aggregation without external text."""
142
+
143
+ checks: tuple[CheckEvidence, ...]
144
+
145
+ def __post_init__(self) -> None:
146
+ """Bound diagnostic evidence and require immutable validated observations."""
147
+
148
+ if (
149
+ not isinstance(self.checks, tuple)
150
+ or len(self.checks) > MAX_CHECKS
151
+ or any(not isinstance(check, CheckEvidence) for check in self.checks)
152
+ ):
153
+ raise IntegrationError("Integration diagnostic evidence exceeds its supported bounds")
154
+
155
+ @property
156
+ def Status(self) -> DiagnosticStatus:
157
+ """Use the established severity contract, including unsupported empty evidence."""
158
+
159
+ return max(
160
+ (check.status for check in self.checks), key=STATUS_PRIORITY.__getitem__,
161
+ default=DiagnosticStatus.UNSUPPORTED,
162
+ )
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class StateEvidence:
167
+ """A local owned-state count makes no remote existence or health assertion."""
168
+
169
+ resource_count: int
170
+
171
+ def __post_init__(self) -> None:
172
+ """Validate the minimal non-identifying inventory summary."""
173
+
174
+ ValidateCount(self.resource_count)
175
+
176
+
177
+ @dataclass(frozen=True, slots=True)
178
+ class ResourceCount:
179
+ """One stable lifecycle observation category without resource locators."""
180
+
181
+ status: str
182
+ count: int
183
+
184
+ def __post_init__(self) -> None:
185
+ """Reject arbitrary provider text and unbounded lifecycle category counts."""
186
+
187
+ if not isinstance(self.status, str) or self.status not in RESOURCE_STATUSES:
188
+ raise IntegrationError("Integration lifecycle resource status is unsupported")
189
+
190
+ ValidateCount(self.count)
191
+
192
+
193
+ @dataclass(frozen=True, slots=True)
194
+ class LifecycleEvidence:
195
+ """Historical lifecycle outcome and recovery requirement convey no mutation permission."""
196
+
197
+ action: str
198
+ status: str
199
+ recovery_required: bool
200
+ resources: tuple[ResourceCount, ...]
201
+
202
+ def __post_init__(self) -> None:
203
+ """Require only known lifecycle actions, outcomes, and immutable aggregate counts."""
204
+
205
+ if (
206
+ not isinstance(self.action, str) or self.action not in LIFECYCLE_ACTIONS
207
+ or not isinstance(self.status, str) or self.status not in LIFECYCLE_STATUSES
208
+ or type(self.recovery_required) is not bool
209
+ or not isinstance(self.resources, tuple)
210
+ or len(self.resources) > len(RESOURCE_STATUSES)
211
+ or any(not isinstance(resource, ResourceCount) for resource in self.resources)
212
+ or len({resource.status for resource in self.resources}) != len(self.resources)
213
+ ):
214
+ raise IntegrationError("Integration lifecycle evidence fields do not match the contract")
215
+
216
+ ValidateCount(sum(resource.count for resource in self.resources))
217
+
218
+
219
+ Evidence: TypeAlias = DiagnosticEvidence | StateEvidence | LifecycleEvidence
220
+
221
+
222
+ @dataclass(frozen=True, slots=True)
223
+ class IntegrationResult:
224
+ """Allowlisted evidence bound to the exact original validated observation request."""
225
+
226
+ request: IntegrationRequest
227
+ evidence: Evidence
228
+
229
+ def __post_init__(self) -> None:
230
+ """Prevent operation and evidence type mismatches during direct construction."""
231
+
232
+ if not isinstance(self.request, IntegrationRequest):
233
+ raise IntegrationError("Integration result requires a validated request")
234
+
235
+ expected_type: type[Evidence] = {
236
+ IntegrationOperation.STATE: StateEvidence,
237
+ IntegrationOperation.LIFECYCLE_REPORT: LifecycleEvidence,
238
+ }.get(self.request.operation, DiagnosticEvidence)
239
+
240
+ if not isinstance(self.evidence, expected_type):
241
+ raise IntegrationError("Integration evidence does not match the requested operation")