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.
- f_layer-2.0.0.dist-info/METADATA +232 -0
- f_layer-2.0.0.dist-info/RECORD +45 -0
- f_layer-2.0.0.dist-info/WHEEL +4 -0
- f_layer-2.0.0.dist-info/entry_points.txt +2 -0
- f_layer-2.0.0.dist-info/licenses/LICENSE +201 -0
- flayer/__init__.py +5 -0
- flayer/__main__.py +247 -0
- flayer/_vpn_guest.py +130 -0
- flayer/core/__init__.py +1 -0
- flayer/core/config.py +198 -0
- flayer/core/contracts.py +114 -0
- flayer/core/lifecycle.py +767 -0
- flayer/core/state.py +351 -0
- flayer/diagnostics/__init__.py +20 -0
- flayer/diagnostics/benchmark.py +272 -0
- flayer/diagnostics/contracts.py +167 -0
- flayer/diagnostics/health.py +154 -0
- flayer/diagnostics/reporting.py +24 -0
- flayer/extensions/__init__.py +25 -0
- flayer/extensions/builtins.py +165 -0
- flayer/extensions/contracts.py +196 -0
- flayer/extensions/discovery.py +152 -0
- flayer/extensions/registry.py +347 -0
- flayer/guest.py +270 -0
- flayer/integrations/__init__.py +23 -0
- flayer/integrations/adapters.py +92 -0
- flayer/integrations/contracts.py +241 -0
- flayer/integrations/wire.py +273 -0
- flayer/profiles/__init__.py +33 -0
- flayer/profiles/_amnezia_export.py +176 -0
- flayer/profiles/_vless_guest.py +432 -0
- flayer/profiles/amneziawg.py +555 -0
- flayer/profiles/artifacts.py +609 -0
- flayer/profiles/gateway.py +729 -0
- flayer/profiles/static_site.py +596 -0
- flayer/profiles/vless.py +422 -0
- flayer/profiles/vpn.py +524 -0
- flayer/providers/__init__.py +1 -0
- flayer/providers/contracts.py +207 -0
- flayer/providers/lifecycle.py +177 -0
- flayer/providers/yandex.py +387 -0
- flayer/providers/yandex_lifecycle.py +781 -0
- flayer/py.typed +0 -0
- flayer/vpn_export.py +72 -0
- flayer/vpn_project.py +828 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Provider-neutral contracts for bounded, read-only infrastructure discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
IDENTIFIER_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceKind(StrEnum):
|
|
14
|
+
"""Infrastructure resource kinds supported by the discovery contract."""
|
|
15
|
+
|
|
16
|
+
INSTANCE = "instance"
|
|
17
|
+
DISK = "disk"
|
|
18
|
+
NETWORK = "network"
|
|
19
|
+
SUBNET = "subnet"
|
|
20
|
+
ADDRESS = "address"
|
|
21
|
+
SECURITY_GROUP = "security_group"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProviderCapability(StrEnum):
|
|
25
|
+
"""Implemented operations advertised by a provider, independent of permissions."""
|
|
26
|
+
|
|
27
|
+
INVENTORY = "inventory"
|
|
28
|
+
RESOURCE_LOOKUP = "resource_lookup"
|
|
29
|
+
CREATE_RESOURCE = "create_resource"
|
|
30
|
+
DELETE_RESOURCE = "delete_resource"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProviderErrorCode(StrEnum):
|
|
34
|
+
"""Stable failure categories that never include vendor output or credentials."""
|
|
35
|
+
|
|
36
|
+
UNAVAILABLE = "unavailable"
|
|
37
|
+
AUTHENTICATION = "authentication"
|
|
38
|
+
PERMISSION_DENIED = "permission_denied"
|
|
39
|
+
NOT_FOUND = "not_found"
|
|
40
|
+
TIMEOUT = "timeout"
|
|
41
|
+
THROTTLED = "throttled"
|
|
42
|
+
CONFLICT = "conflict"
|
|
43
|
+
SCOPE_MISMATCH = "scope_mismatch"
|
|
44
|
+
INVALID_RESPONSE = "invalid_response"
|
|
45
|
+
UNSUPPORTED = "unsupported"
|
|
46
|
+
COMMAND_FAILED = "command_failed"
|
|
47
|
+
INCOMPLETE_INVENTORY = "incomplete_inventory"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProviderError(RuntimeError):
|
|
51
|
+
"""A sanitized provider failure with a stable code and retry recommendation."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, code: ProviderErrorCode, operation: str) -> None:
|
|
54
|
+
"""Build an error without retaining external command output or exceptions."""
|
|
55
|
+
|
|
56
|
+
self.code = code
|
|
57
|
+
self.operation = operation
|
|
58
|
+
self.retryable = code in {
|
|
59
|
+
ProviderErrorCode.TIMEOUT,
|
|
60
|
+
ProviderErrorCode.THROTTLED,
|
|
61
|
+
}
|
|
62
|
+
super().__init__(f"Provider operation {operation} failed: {code.value}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ValidateIdentifier(value: str) -> None:
|
|
66
|
+
"""Reject unsafe identifiers without including the rejected value in errors."""
|
|
67
|
+
|
|
68
|
+
if not isinstance(value, str) or IDENTIFIER_PATTERN.fullmatch(value) is None:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Provider identifiers require 1-128 ASCII letters, digits, dots, underscores or dashes"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class ProviderIdentity:
|
|
76
|
+
"""Non-secret provider and explicit scope identity; credentials remain external."""
|
|
77
|
+
|
|
78
|
+
provider_id: str
|
|
79
|
+
scope_id: str
|
|
80
|
+
authentication_source: str
|
|
81
|
+
|
|
82
|
+
def __post_init__(self) -> None:
|
|
83
|
+
"""Validate immutable identity fields before they can define a resource scope."""
|
|
84
|
+
|
|
85
|
+
ValidateIdentifier(self.provider_id)
|
|
86
|
+
ValidateIdentifier(self.scope_id)
|
|
87
|
+
ValidateIdentifier(self.authentication_source)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class ResourceReference:
|
|
92
|
+
"""A resource key bound to one provider, one scope and one resource kind."""
|
|
93
|
+
|
|
94
|
+
provider_id: str
|
|
95
|
+
scope_id: str
|
|
96
|
+
kind: ResourceKind
|
|
97
|
+
resource_id: str
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
"""Reject malformed identities and unrecognized resource kinds."""
|
|
101
|
+
|
|
102
|
+
ValidateIdentifier(self.provider_id)
|
|
103
|
+
ValidateIdentifier(self.scope_id)
|
|
104
|
+
ValidateIdentifier(self.resource_id)
|
|
105
|
+
|
|
106
|
+
if not isinstance(self.kind, ResourceKind):
|
|
107
|
+
raise ValueError("Resource kind must be a ResourceKind member")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class ProviderResource:
|
|
112
|
+
"""A normalized observation, excluding raw vendor payloads and instance metadata."""
|
|
113
|
+
|
|
114
|
+
reference: ResourceReference
|
|
115
|
+
name: str
|
|
116
|
+
status: str = "UNKNOWN"
|
|
117
|
+
zone_id: str | None = None
|
|
118
|
+
labels: tuple[tuple[str, str], ...] = ()
|
|
119
|
+
public_addresses: tuple[str, ...] = ()
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
"""Require immutable labels and endpoint collections from provider adapters."""
|
|
123
|
+
|
|
124
|
+
if not isinstance(self.reference, ResourceReference):
|
|
125
|
+
raise ValueError("Provider resources require a validated resource reference")
|
|
126
|
+
|
|
127
|
+
if not isinstance(self.name, str) or not isinstance(self.status, str):
|
|
128
|
+
raise ValueError("Resource names and statuses must be strings")
|
|
129
|
+
|
|
130
|
+
if self.zone_id is not None:
|
|
131
|
+
ValidateIdentifier(self.zone_id)
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
not isinstance(self.labels, tuple)
|
|
135
|
+
or any(
|
|
136
|
+
not isinstance(label, tuple)
|
|
137
|
+
or len(label) != 2
|
|
138
|
+
or not all(isinstance(value, str) for value in label)
|
|
139
|
+
for label in self.labels
|
|
140
|
+
)
|
|
141
|
+
or len({label[0] for label in self.labels}) != len(self.labels)
|
|
142
|
+
):
|
|
143
|
+
raise ValueError("Resource labels must be immutable unique string pairs")
|
|
144
|
+
|
|
145
|
+
if not isinstance(self.public_addresses, tuple) or not all(
|
|
146
|
+
isinstance(address, str) for address in self.public_addresses
|
|
147
|
+
):
|
|
148
|
+
raise ValueError("Public addresses must be an immutable string tuple")
|
|
149
|
+
|
|
150
|
+
def HasLabels(self, expected: tuple[tuple[str, str], ...]) -> bool:
|
|
151
|
+
"""Check all required labels without treating a resource name as ownership."""
|
|
152
|
+
|
|
153
|
+
actual = dict(self.labels)
|
|
154
|
+
|
|
155
|
+
return bool(expected) and all(actual.get(key) == value for key, value in expected)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class ProviderStatus:
|
|
160
|
+
"""A sanitized probe result; unknown authentication differs from a failed probe."""
|
|
161
|
+
|
|
162
|
+
identity: ProviderIdentity
|
|
163
|
+
available: bool
|
|
164
|
+
authenticated: bool | None = None
|
|
165
|
+
error_code: ProviderErrorCode | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@runtime_checkable
|
|
169
|
+
class CloudProvider(Protocol):
|
|
170
|
+
"""Read-only provider boundary consumed by diagnostics and future orchestration."""
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def Identity(self) -> ProviderIdentity:
|
|
174
|
+
"""Return the provider identity and explicit discovery scope."""
|
|
175
|
+
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def Capabilities(self) -> frozenset[ProviderCapability]:
|
|
180
|
+
"""Advertise implemented operations without promising account authorization."""
|
|
181
|
+
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
def CheckAvailability(self) -> ProviderStatus:
|
|
185
|
+
"""Probe the local provider adapter without accessing infrastructure."""
|
|
186
|
+
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
def CheckAuthentication(self) -> ProviderStatus:
|
|
190
|
+
"""Probe read permission to the selected scope without exposing credentials."""
|
|
191
|
+
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
def ListResources(self, kind: ResourceKind) -> tuple[ProviderResource, ...]:
|
|
195
|
+
"""List a complete resource kind within the provider's explicit scope."""
|
|
196
|
+
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
def GetResource(self, reference: ResourceReference) -> ProviderResource:
|
|
200
|
+
"""Read one resource and reject any reference or response outside the scope."""
|
|
201
|
+
|
|
202
|
+
...
|
|
203
|
+
|
|
204
|
+
def DiscoverInventory(self) -> tuple[ProviderResource, ...]:
|
|
205
|
+
"""Return all supported kinds or fail without returning partial inventory."""
|
|
206
|
+
|
|
207
|
+
...
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Immutable lifecycle requests and explicit, ownership-bound mutation contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Protocol, TypeAlias, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from flayer.core.contracts import StackIdentity, ValidateName
|
|
12
|
+
|
|
13
|
+
from .contracts import (
|
|
14
|
+
ProviderCapability,
|
|
15
|
+
ProviderError,
|
|
16
|
+
ProviderErrorCode,
|
|
17
|
+
ProviderIdentity,
|
|
18
|
+
ProviderResource,
|
|
19
|
+
ResourceKind,
|
|
20
|
+
ResourceReference,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
JsonValue: TypeAlias = str | int | bool | None | tuple["JsonValue", ...]
|
|
24
|
+
MAX_PARAMETER_DEPTH = 10
|
|
25
|
+
MAX_PARAMETER_VALUES = 2048
|
|
26
|
+
LOGICAL_ID_LABEL = "flayer-resource"
|
|
27
|
+
SPEC_DIGEST_LABEL = "flayer-spec"
|
|
28
|
+
OPERATION_ID_LABEL = "flayer-operation"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ValidateValue(value: JsonValue, depth: int, remaining: list[int]) -> None:
|
|
32
|
+
"""Bound recursively immutable values before hashing or translating requests."""
|
|
33
|
+
|
|
34
|
+
remaining[0] -= 1
|
|
35
|
+
|
|
36
|
+
if depth > MAX_PARAMETER_DEPTH or remaining[0] < 0:
|
|
37
|
+
raise ValueError("Resource parameters exceed their structural bounds")
|
|
38
|
+
|
|
39
|
+
if isinstance(value, tuple):
|
|
40
|
+
for item in value:
|
|
41
|
+
_ValidateValue(item, depth + 1, remaining)
|
|
42
|
+
|
|
43
|
+
elif isinstance(value, str):
|
|
44
|
+
if len(value) > 16384 or "\x00" in value:
|
|
45
|
+
raise ValueError("Resource parameter strings exceed their bounds")
|
|
46
|
+
|
|
47
|
+
elif value is not None and type(value) not in {int, bool}:
|
|
48
|
+
raise ValueError("Resource parameters require immutable JSON-compatible values")
|
|
49
|
+
|
|
50
|
+
elif type(value) is int and not -(2 ** 63) <= value < 2 ** 63:
|
|
51
|
+
raise ValueError("Resource integer parameters exceed signed 64-bit bounds")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ResourceSpec:
|
|
56
|
+
"""A desired logical resource with immutable options and explicit dependencies."""
|
|
57
|
+
|
|
58
|
+
logical_id: str
|
|
59
|
+
kind: ResourceKind
|
|
60
|
+
name: str
|
|
61
|
+
parameters: tuple[tuple[str, JsonValue], ...] = field(default=(), repr=False)
|
|
62
|
+
dependencies: tuple[str, ...] = ()
|
|
63
|
+
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
"""Reject malformed, mutable, duplicate or cyclic direct request fields."""
|
|
66
|
+
|
|
67
|
+
ValidateName(self.logical_id, "resource.logical_id")
|
|
68
|
+
ValidateName(self.name, "resource.name")
|
|
69
|
+
|
|
70
|
+
if not isinstance(self.kind, ResourceKind):
|
|
71
|
+
raise ValueError("Lifecycle kind requires a ResourceKind member")
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
not isinstance(self.parameters, tuple)
|
|
75
|
+
or len(self.parameters) > 128
|
|
76
|
+
or any(
|
|
77
|
+
not isinstance(pair, tuple) or len(pair) != 2
|
|
78
|
+
or not isinstance(pair[0], str) or not pair[0].isidentifier()
|
|
79
|
+
for pair in self.parameters
|
|
80
|
+
)
|
|
81
|
+
or len({pair[0] for pair in self.parameters}) != len(self.parameters)
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("Resource parameters require unique immutable named pairs")
|
|
84
|
+
|
|
85
|
+
remaining = [MAX_PARAMETER_VALUES]
|
|
86
|
+
|
|
87
|
+
for _, value in self.parameters:
|
|
88
|
+
_ValidateValue(value, 0, remaining)
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
not isinstance(self.dependencies, tuple)
|
|
92
|
+
or len(self.dependencies) > 128
|
|
93
|
+
or any(not isinstance(value, str) for value in self.dependencies)
|
|
94
|
+
or len(set(self.dependencies)) != len(self.dependencies)
|
|
95
|
+
or self.logical_id in self.dependencies
|
|
96
|
+
):
|
|
97
|
+
raise ValueError("Resource dependencies require unique immutable logical IDs")
|
|
98
|
+
|
|
99
|
+
for logical_id in self.dependencies:
|
|
100
|
+
ValidateName(logical_id, "resource.dependency")
|
|
101
|
+
|
|
102
|
+
def OwnershipLabels(self, identity: StackIdentity) -> tuple[tuple[str, str], ...]:
|
|
103
|
+
"""Bind one logical resource to the complete stack ownership boundary."""
|
|
104
|
+
|
|
105
|
+
labels = identity.OwnershipLabels()
|
|
106
|
+
labels[LOGICAL_ID_LABEL] = self.logical_id
|
|
107
|
+
content = (self.logical_id, self.kind.value, self.name, self.parameters, self.dependencies)
|
|
108
|
+
digest = hashlib.sha256(json.dumps(content, sort_keys=True).encode("utf-8")).hexdigest()
|
|
109
|
+
labels[SPEC_DIGEST_LABEL] = digest[:40]
|
|
110
|
+
|
|
111
|
+
return tuple(sorted(labels.items()))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MutationError(ProviderError):
|
|
115
|
+
"""A sanitized mutation failure that distinguishes an uncertain remote outcome."""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, code: ProviderErrorCode, operation: str, outcome_unknown: bool
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Retain only a stable failure category and whether submission may have occurred."""
|
|
121
|
+
|
|
122
|
+
if type(outcome_unknown) is not bool:
|
|
123
|
+
raise ValueError("Mutation outcome uncertainty requires a boolean")
|
|
124
|
+
|
|
125
|
+
self.outcome_unknown = outcome_unknown
|
|
126
|
+
super().__init__(code, operation)
|
|
127
|
+
self.retryable = self.retryable and not outcome_unknown
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@runtime_checkable
|
|
131
|
+
class LifecycleProvider(Protocol):
|
|
132
|
+
"""Mutation boundary with exact scope, stable reconciliation and owned deletion."""
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def Identity(self) -> ProviderIdentity:
|
|
136
|
+
"""Return the provider and explicit scope used by every observation and command."""
|
|
137
|
+
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def Capabilities(self) -> frozenset[ProviderCapability]:
|
|
142
|
+
"""Advertise implemented mutation operations independently of account permissions."""
|
|
143
|
+
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
def ValidateSpec(self, spec: ResourceSpec) -> None:
|
|
147
|
+
"""Reject unsupported resource options without accessing infrastructure."""
|
|
148
|
+
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
def FindResource(
|
|
152
|
+
self, spec: ResourceSpec, identity: StackIdentity
|
|
153
|
+
) -> ProviderResource | None:
|
|
154
|
+
"""Find the unique complete ownership-label match or reject ambiguous inventory."""
|
|
155
|
+
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
def GetResource(self, reference: ResourceReference) -> ProviderResource:
|
|
159
|
+
"""Observe an exact reference with scope validation and sanitized failures."""
|
|
160
|
+
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
def CreateResource(
|
|
164
|
+
self, spec: ResourceSpec, identity: StackIdentity,
|
|
165
|
+
dependencies: Mapping[str, ProviderResource], operation_id: str,
|
|
166
|
+
) -> ProviderResource:
|
|
167
|
+
"""Create once from validated owned dependencies; never retry uncertain creation."""
|
|
168
|
+
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
def DeleteResource(
|
|
172
|
+
self, reference: ResourceReference, identity: StackIdentity, logical_id: str,
|
|
173
|
+
operation_id: str | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Re-read exact scope and complete ownership immediately before deleting by ID."""
|
|
176
|
+
|
|
177
|
+
...
|