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
@@ -0,0 +1,167 @@
1
+ """Provider-independent diagnostic results and output safety contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+
11
+ REDACTED = "[redacted]"
12
+ SECRET_KEY = re.compile(
13
+ r"password|passwd|secret|token|credential|authorization|cookie|private.?key|api.?key|"
14
+ r"ssh.?key|client.?key|access.?key|refresh.?key",
15
+ re.IGNORECASE,
16
+ )
17
+ SECRET_VALUE = re.compile(
18
+ r"(?:bearer\s+\S+|-----BEGIN[^\n]*PRIVATE KEY-----[\s\S]*|"
19
+ r"(?:password|passwd|secret|token|api[_-]?key)\s*[=:][^\n]*|"
20
+ r"https?://[^\s]*|(?:\d{1,3}\.){3}\d{1,3})",
21
+ re.IGNORECASE,
22
+ )
23
+ SAFE_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,63}\Z")
24
+
25
+
26
+ class DiagnosticStatus(str, Enum):
27
+ """Stable outcomes distinguish failure, expired evidence, and missing support."""
28
+
29
+ OK = "ok"
30
+ WARNING = "warning"
31
+ FAILED = "failed"
32
+ STALE = "stale"
33
+ UNSUPPORTED = "unsupported"
34
+
35
+
36
+ def Redact(value: object, _depth: int = 0) -> object:
37
+ """Recursively redact credential fields and common sensitive text before output."""
38
+
39
+ if _depth >= 16:
40
+ return REDACTED
41
+
42
+ if isinstance(value, Mapping):
43
+ sanitized: dict[str, object] = {}
44
+
45
+ for key, item in value.items():
46
+ sensitive = not isinstance(key, str) or bool(SECRET_KEY.search(key))
47
+ safe_key = key if isinstance(key, str) and SAFE_KEY.fullmatch(key) else REDACTED
48
+
49
+ if sensitive:
50
+ safe_key = REDACTED
51
+
52
+ candidate = safe_key
53
+ suffix = 2
54
+
55
+ while candidate in sanitized:
56
+ candidate = f"{safe_key}:{suffix}"
57
+ suffix += 1
58
+
59
+ sanitized[candidate] = REDACTED if sensitive else Redact(item, _depth + 1)
60
+
61
+ return sanitized
62
+
63
+ if isinstance(value, str):
64
+ return SECRET_VALUE.sub(REDACTED, value)
65
+
66
+ if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)):
67
+ return [Redact(item, _depth + 1) for item in value]
68
+
69
+ if isinstance(value, bool) or value is None or isinstance(value, int):
70
+ return value
71
+
72
+ if isinstance(value, float):
73
+ return value if math.isfinite(value) else None
74
+
75
+ return REDACTED
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class CheckResult:
80
+ """One diagnostic observation with details that are sanitized at serialization."""
81
+
82
+ name: str
83
+ status: DiagnosticStatus
84
+ message: str
85
+ details: Mapping[str, object] = field(default_factory=dict)
86
+
87
+ def __post_init__(self) -> None:
88
+ """Reject malformed adapter results before they can break report rendering."""
89
+
90
+ if (
91
+ not isinstance(self.name, str)
92
+ or not self.name
93
+ or not isinstance(self.status, DiagnosticStatus)
94
+ or not isinstance(self.message, str)
95
+ or not isinstance(self.details, Mapping)
96
+ ):
97
+ raise ValueError("Diagnostic check fields do not match the result contract")
98
+
99
+ def AsDict(self) -> dict[str, object]:
100
+ """Return stable JSON-compatible fields without arbitrary raw provider output."""
101
+
102
+ return {
103
+ "name": Redact(self.name),
104
+ "status": self.status.value,
105
+ "message": Redact(self.message),
106
+ "details": Redact(self.details),
107
+ }
108
+
109
+
110
+ STATUS_PRIORITY = {
111
+ DiagnosticStatus.OK: 0,
112
+ DiagnosticStatus.WARNING: 1,
113
+ DiagnosticStatus.UNSUPPORTED: 2,
114
+ DiagnosticStatus.STALE: 3,
115
+ DiagnosticStatus.FAILED: 4,
116
+ }
117
+ EXIT_CODES = {
118
+ DiagnosticStatus.OK: 0,
119
+ DiagnosticStatus.WARNING: 1,
120
+ DiagnosticStatus.FAILED: 1,
121
+ DiagnosticStatus.STALE: 3,
122
+ DiagnosticStatus.UNSUPPORTED: 4,
123
+ }
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class DiagnosticReport:
128
+ """Versioned report whose aggregate outcome never hides failed checks."""
129
+
130
+ command: str
131
+ checks: tuple[CheckResult, ...]
132
+
133
+ def __post_init__(self) -> None:
134
+ """Keep report aggregation predictable and reject malformed public inputs."""
135
+
136
+ if (
137
+ not isinstance(self.command, str)
138
+ or not self.command
139
+ or not isinstance(self.checks, tuple)
140
+ or not all(isinstance(check, CheckResult) for check in self.checks)
141
+ ):
142
+ raise ValueError("Diagnostic report fields do not match the report contract")
143
+
144
+ @property
145
+ def Status(self) -> DiagnosticStatus:
146
+ """Return the most severe result, treating an empty report as unsupported."""
147
+
148
+ return max(
149
+ (check.status for check in self.checks),
150
+ key=STATUS_PRIORITY.__getitem__,
151
+ default=DiagnosticStatus.UNSUPPORTED,
152
+ )
153
+
154
+ def ExitCode(self) -> int:
155
+ """Return zero only when every requested observation succeeds."""
156
+
157
+ return EXIT_CODES[self.Status]
158
+
159
+ def AsDict(self) -> dict[str, object]:
160
+ """Expose the report schema and sanitized observations in deterministic order."""
161
+
162
+ return {
163
+ "schema_version": 1,
164
+ "command": Redact(self.command),
165
+ "status": self.Status.value,
166
+ "checks": [check.AsDict() for check in self.checks],
167
+ }
@@ -0,0 +1,154 @@
1
+ """Injected health probes and freshness validation without cloud access assumptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import sys
7
+ import time
8
+ from collections.abc import Iterable, Mapping
9
+ from typing import Protocol
10
+
11
+ from .contracts import CheckResult, DiagnosticStatus
12
+
13
+
14
+ class HealthProbe(Protocol):
15
+ """An adapter supplies read-only observations without transferring authorization."""
16
+
17
+ def __call__(self) -> CheckResult:
18
+ """Perform an adapter-bounded probe and return one structured observation."""
19
+
20
+ ...
21
+
22
+
23
+ def RuntimeChecks() -> tuple[CheckResult, ...]:
24
+ """Check only supported Python execution; never imply remote infrastructure health."""
25
+
26
+ supported = sys.version_info >= (3, 11)
27
+
28
+ return (
29
+ CheckResult(
30
+ "python-runtime",
31
+ DiagnosticStatus.OK if supported else DiagnosticStatus.FAILED,
32
+ "Python runtime is supported" if supported else "Python 3.11 or newer is required",
33
+ {"version": ".".join(str(part) for part in sys.version_info[:3])},
34
+ ),
35
+ )
36
+
37
+
38
+ def RunChecks(probes: Iterable[HealthProbe]) -> tuple[CheckResult, ...]:
39
+ """Collect injected observations and suppress raw exception text on probe failure."""
40
+
41
+ checks: list[CheckResult] = []
42
+
43
+ for probe in probes:
44
+ try:
45
+ result = probe()
46
+
47
+ if not isinstance(result, CheckResult):
48
+ raise TypeError("Probe must return CheckResult")
49
+
50
+ checks.append(result)
51
+
52
+ except Exception:
53
+ checks.append(CheckResult(
54
+ "probe", DiagnosticStatus.FAILED, "Probe failed without safe structured evidence"
55
+ ))
56
+
57
+ if not checks:
58
+ checks.append(CheckResult(
59
+ "probe", DiagnosticStatus.UNSUPPORTED, "No health probe was supplied"
60
+ ))
61
+
62
+ return tuple(checks)
63
+
64
+
65
+ def ProviderStatus(observation: Mapping[str, object] | None = None) -> CheckResult:
66
+ """Interpret a supplied generic provider observation without discovering resources."""
67
+
68
+ if observation is None:
69
+ return CheckResult(
70
+ "provider-state", DiagnosticStatus.UNSUPPORTED, "No provider observation was supplied"
71
+ )
72
+
73
+ if not isinstance(observation, Mapping):
74
+ return CheckResult(
75
+ "provider-state", DiagnosticStatus.FAILED, "Provider observation must be a mapping"
76
+ )
77
+
78
+ state = observation.get("state")
79
+
80
+ if state not in ("ready", "pending", "failed", "absent"):
81
+ return CheckResult(
82
+ "provider-state", DiagnosticStatus.FAILED, "Provider observation has an invalid state"
83
+ )
84
+
85
+ status = {
86
+ "ready": DiagnosticStatus.OK,
87
+ "pending": DiagnosticStatus.WARNING,
88
+ "failed": DiagnosticStatus.FAILED,
89
+ "absent": DiagnosticStatus.FAILED,
90
+ }[str(state)]
91
+
92
+ return CheckResult("provider-state", status, "Provider state observed", {"state": state})
93
+
94
+
95
+ def EvaluateHealthReport(
96
+ report: Mapping[str, object] | None,
97
+ max_age_seconds: float = 300,
98
+ current_time: float | None = None,
99
+ ) -> CheckResult:
100
+ """Validate versioned health evidence and separate stale evidence from failed health."""
101
+
102
+ now = time.time() if current_time is None else current_time
103
+
104
+ if (
105
+ isinstance(max_age_seconds, bool)
106
+ or not isinstance(max_age_seconds, (int, float))
107
+ or not math.isfinite(max_age_seconds)
108
+ or max_age_seconds <= 0
109
+ or isinstance(now, bool)
110
+ or not isinstance(now, (int, float))
111
+ or not math.isfinite(now)
112
+ ):
113
+ raise ValueError("Health freshness bounds must be finite and positive")
114
+
115
+ if report is None:
116
+ return CheckResult(
117
+ "health-report", DiagnosticStatus.UNSUPPORTED, "No health report was supplied"
118
+ )
119
+
120
+ if not isinstance(report, Mapping):
121
+ return CheckResult(
122
+ "health-report", DiagnosticStatus.FAILED, "Health report must be a mapping"
123
+ )
124
+
125
+ if type(report.get("schema_version")) is not int or report.get("schema_version") != 1:
126
+ return CheckResult(
127
+ "health-report", DiagnosticStatus.UNSUPPORTED, "Health report schema is unsupported"
128
+ )
129
+
130
+ timestamp = report.get("timestamp")
131
+ outcome = report.get("status")
132
+
133
+ if (
134
+ isinstance(timestamp, bool)
135
+ or not isinstance(timestamp, (int, float))
136
+ or not math.isfinite(timestamp)
137
+ or outcome not in ("ok", "warning", "failed")
138
+ ):
139
+ return CheckResult(
140
+ "health-report", DiagnosticStatus.FAILED, "Health report fields are invalid"
141
+ )
142
+
143
+ age = now - timestamp
144
+ details: dict[str, object] = {"age_seconds": age, "max_age_seconds": max_age_seconds}
145
+
146
+ if age < 0 or age > max_age_seconds:
147
+ return CheckResult(
148
+ "health-report", DiagnosticStatus.STALE,
149
+ "Health report is expired or dated in the future", details,
150
+ )
151
+
152
+ return CheckResult(
153
+ "health-report", DiagnosticStatus(str(outcome)), "Health report is current", details
154
+ )
@@ -0,0 +1,24 @@
1
+ """Stable JSON and concise text representations of sanitized diagnostic reports."""
2
+
3
+ import json
4
+
5
+ from .contracts import DiagnosticReport
6
+
7
+
8
+ def RenderJson(report: DiagnosticReport) -> str:
9
+ """Serialize only the versioned public report with deterministic JSON keys."""
10
+
11
+ return json.dumps(report.AsDict(), sort_keys=True, allow_nan=False)
12
+
13
+
14
+ def RenderText(report: DiagnosticReport) -> str:
15
+ """Render one safe single-line observation per check without raw details."""
16
+
17
+ public = report.AsDict()
18
+ lines = [f"{public['command']}: {public['status']}"]
19
+
20
+ for check in report.checks:
21
+ safe = check.AsDict()
22
+ lines.append(f"[{safe['status']}] {safe['name']}: {safe['message']}")
23
+
24
+ return "\n".join(" ".join(line.split()) for line in lines)
@@ -0,0 +1,25 @@
1
+ """Additive typed extension contracts, static metadata discovery, and explicit loading."""
2
+
3
+ from .builtins import BuiltinDescriptors
4
+ from .contracts import (
5
+ PROFILE_GROUP,
6
+ PROVIDER_GROUP,
7
+ CompiledProfile,
8
+ ExtensionDescriptor,
9
+ ExtensionError,
10
+ ExtensionKind,
11
+ LifecycleProviderExtension,
12
+ ProfileContext,
13
+ ProfileExtension,
14
+ ProviderContext,
15
+ ProviderExtension,
16
+ )
17
+ from .discovery import DiscoverExtensions
18
+ from .registry import ExtensionRegistry
19
+
20
+ __all__ = [
21
+ "PROFILE_GROUP", "PROVIDER_GROUP", "BuiltinDescriptors", "CompiledProfile",
22
+ "DiscoverExtensions", "ExtensionDescriptor", "ExtensionError", "ExtensionKind",
23
+ "ExtensionRegistry", "LifecycleProviderExtension", "ProfileContext", "ProfileExtension",
24
+ "ProviderContext", "ProviderExtension",
25
+ ]
@@ -0,0 +1,165 @@
1
+ """Usable extension adapters for the existing Yandex providers and secure gateway."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from flayer import __version__
11
+ from flayer.core.contracts import StackIdentity
12
+ from flayer.profiles.artifacts import ArtifactBundle
13
+ from flayer.providers.contracts import CloudProvider
14
+ from flayer.providers.lifecycle import LifecycleProvider
15
+
16
+ from .contracts import (
17
+ PROFILE_GROUP,
18
+ PROVIDER_GROUP,
19
+ CompiledProfile,
20
+ ExtensionDescriptor,
21
+ ExtensionError,
22
+ ExtensionKind,
23
+ ProfileContext,
24
+ ProfileExtension,
25
+ ProviderContext,
26
+ ProviderExtension,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from flayer.profiles.gateway import GatewayPlan
31
+
32
+ YANDEX_READ_ONLY = ExtensionDescriptor(
33
+ ExtensionKind.PROVIDER, "yandex-read-only", "f-layer", __version__, PROVIDER_GROUP,
34
+ "flayer.extensions.builtins:YandexReadOnlyExtension",
35
+ )
36
+ YANDEX_LIFECYCLE = ExtensionDescriptor(
37
+ ExtensionKind.PROVIDER, "yandex-lifecycle", "f-layer", __version__, PROVIDER_GROUP,
38
+ "flayer.extensions.builtins:YandexLifecycleExtension",
39
+ )
40
+ SECURE_GATEWAY = ExtensionDescriptor(
41
+ ExtensionKind.PROFILE, "secure-gateway", "f-layer", __version__, PROFILE_GROUP,
42
+ "flayer.extensions.builtins:SecureGatewayExtension",
43
+ )
44
+
45
+
46
+ def BuiltinDescriptors() -> tuple[ExtensionDescriptor, ...]:
47
+ """Return static descriptors without importing provider or profile implementations."""
48
+
49
+ return (YANDEX_READ_ONLY, YANDEX_LIFECYCLE, SECURE_GATEWAY)
50
+
51
+
52
+ class YandexReadOnlyExtension:
53
+ """Build the real read-only Yandex adapter with an external CLI profile reference."""
54
+
55
+ @property
56
+ def Descriptor(self) -> ExtensionDescriptor:
57
+ """Return the immutable built-in identity used by registry lookup."""
58
+
59
+ return YANDEX_READ_ONLY
60
+
61
+ def CreateProvider(self, context: ProviderContext) -> CloudProvider:
62
+ """Construct a real adapter without running CLI commands or reading credentials."""
63
+
64
+ from flayer.providers.yandex import YandexCloudProvider, YandexCloudSettings
65
+
66
+ if not isinstance(context, ProviderContext):
67
+ raise ExtensionError("Provider construction requires a validated ProviderContext")
68
+
69
+ return YandexCloudProvider(YandexCloudSettings(
70
+ folder_id=context.scope_id, profile=context.authentication_reference,
71
+ command_timeout=context.command_timeout, inventory_limit=context.inventory_limit,
72
+ ))
73
+
74
+
75
+ class YandexLifecycleExtension(YandexReadOnlyExtension):
76
+ """Opt in separately to the existing exact-scope owned resource mutation contract."""
77
+
78
+ @property
79
+ def Descriptor(self) -> ExtensionDescriptor:
80
+ """Return the distinct identity that explicitly advertises lifecycle construction."""
81
+
82
+ return YANDEX_LIFECYCLE
83
+
84
+ def CreateLifecycleProvider(self, context: ProviderContext) -> LifecycleProvider:
85
+ """Construct the real lifecycle adapter without performing provider operations."""
86
+
87
+ from flayer.providers.yandex import YandexCloudSettings
88
+ from flayer.providers.yandex_lifecycle import YandexLifecycleProvider
89
+
90
+ if not isinstance(context, ProviderContext):
91
+ raise ExtensionError("Provider construction requires a validated ProviderContext")
92
+
93
+ return YandexLifecycleProvider(YandexCloudSettings(
94
+ folder_id=context.scope_id, profile=context.authentication_reference,
95
+ command_timeout=context.command_timeout, inventory_limit=context.inventory_limit,
96
+ ))
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class _CompiledGateway:
101
+ """Adapt a real compiled gateway while retaining its artifact verification contract."""
102
+
103
+ plan: GatewayPlan
104
+
105
+ @property
106
+ def Identity(self) -> StackIdentity:
107
+ """Return the exact ownership identity already validated by profile compilation."""
108
+
109
+ return self.plan.profile.identity
110
+
111
+ def BuildServerBundle(self) -> ArtifactBundle:
112
+ """Generate the real gateway server bundle in memory without storing it."""
113
+
114
+ from flayer.profiles.gateway import BuildServerBundle
115
+
116
+ return BuildServerBundle(self.plan.profile)
117
+
118
+ def RenderPlan(self, *, user_data_file: str | Path) -> str:
119
+ """Preserve gateway verification of the exact exported server artifact."""
120
+
121
+ from flayer.profiles.gateway import RenderPlan
122
+
123
+ return RenderPlan(self.plan, user_data_file=user_data_file)
124
+
125
+
126
+ class SecureGatewayExtension:
127
+ """Compile the real strict gateway schema through the additive extension boundary."""
128
+
129
+ @property
130
+ def Descriptor(self) -> ExtensionDescriptor:
131
+ """Return the immutable built-in profile identity."""
132
+
133
+ return SECURE_GATEWAY
134
+
135
+ def CompileProfile(
136
+ self, configuration: Mapping[str, object], context: ProfileContext,
137
+ ) -> CompiledProfile:
138
+ """Reject identity disagreement before constructing a real gateway plan."""
139
+
140
+ from flayer.profiles.gateway import CompileGateway, ParseGatewayProfile
141
+
142
+ if not isinstance(context, ProfileContext):
143
+ raise ExtensionError("Profile compilation requires a validated ProfileContext")
144
+
145
+ profile = ParseGatewayProfile(configuration)
146
+
147
+ if profile.identity != context.identity:
148
+ raise ExtensionError("Profile configuration does not match its exact ownership context")
149
+
150
+ return _CompiledGateway(CompileGateway(profile))
151
+
152
+
153
+ def LoadBuiltin(descriptor: ExtensionDescriptor) -> ProviderExtension | ProfileExtension:
154
+ """Load only an exact known built-in descriptor, rejecting invented built-in targets."""
155
+
156
+ if descriptor == YANDEX_READ_ONLY:
157
+ return YandexReadOnlyExtension()
158
+
159
+ if descriptor == YANDEX_LIFECYCLE:
160
+ return YandexLifecycleExtension()
161
+
162
+ if descriptor == SECURE_GATEWAY:
163
+ return SecureGatewayExtension()
164
+
165
+ raise ExtensionError("Extension is not an exact registered F-Layer built-in")