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,596 @@
1
+ """Prepare a bounded static HTTP page for an explicitly selected fresh Ubuntu guest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import base64
7
+ import binascii
8
+ import hashlib
9
+ import html
10
+ import ipaddress
11
+ import json
12
+ import os
13
+ import re
14
+ import stat
15
+ import struct
16
+ import tomllib
17
+ import unicodedata
18
+ from collections.abc import Mapping, Sequence
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+
22
+ from flayer.core.contracts import (
23
+ IDENTITY_FIELDS,
24
+ ContractError,
25
+ ParseIdentity,
26
+ RequireTable,
27
+ StackIdentity,
28
+ ValidateFields,
29
+ ValidateIdentifier,
30
+ ValidateName,
31
+ ValidateSchemaVersion,
32
+ )
33
+ from flayer.profiles.artifacts import (
34
+ Artifact,
35
+ ArtifactBundle,
36
+ ReadOwnedArtifactFile,
37
+ RemoveArtifactBundle,
38
+ WriteArtifactBundle,
39
+ )
40
+
41
+ PROFILE_NAME = "static-site"
42
+ GUEST_CONTRACT = "ubuntu-24.04-cloud-init"
43
+ MAX_PROFILE_BYTES = 32768
44
+ CLOUD_INIT_FILENAME = "static-site-cloud-init.json"
45
+ PLAN_FILENAME = "deployment-plan.toml"
46
+ _USERNAME_PATTERN = re.compile(r"[a-z][a-z0-9-]{0,31}\Z")
47
+ _PLACEMENT_FIELDS = frozenset({"zone_id", "image_id", "subnet_cidr"})
48
+ _SIZING_FIELDS = frozenset({"cores", "memory_gib", "boot_disk_gib"})
49
+
50
+
51
+ class StaticSiteError(ContractError):
52
+ """The static site violates its bounded profile or prepared artifact contract."""
53
+
54
+
55
+ def _Integer(value: object, minimum: int, maximum: int, label: str) -> int:
56
+ """Reject coercion and constrain resource requests before external operations."""
57
+
58
+ if type(value) is not int or not minimum <= value <= maximum:
59
+ raise StaticSiteError(f"{label} must be an integer within {minimum}..{maximum}")
60
+
61
+ return value
62
+
63
+
64
+ def _Cidr(value: object, minimum_prefix: int, label: str) -> str:
65
+ """Require an explicit canonical IPv4 network without host bits or IPv6."""
66
+
67
+ if not isinstance(value, str):
68
+ raise StaticSiteError(f"{label} must be a canonical IPv4 CIDR")
69
+
70
+ try:
71
+ network = ipaddress.IPv4Network(value, strict=True)
72
+
73
+ except ValueError:
74
+ raise StaticSiteError(f"{label} must be a canonical IPv4 CIDR") from None
75
+
76
+ if str(network) != value or network.prefixlen < minimum_prefix:
77
+ raise StaticSiteError(f"{label} must use canonical prefix /{minimum_prefix} or narrower")
78
+
79
+ return value
80
+
81
+
82
+ def _Cidrs(value: object, minimum_prefix: int, label: str) -> tuple[str, ...]:
83
+ """Bound rule expansion and reject duplicate or overlapping source networks."""
84
+
85
+ if not isinstance(value, tuple) or not 1 <= len(value) <= 16:
86
+ raise StaticSiteError(f"{label} must contain 1..16 immutable CIDRs")
87
+
88
+ cidrs = tuple(_Cidr(item, minimum_prefix, label) for item in value)
89
+ networks = tuple(ipaddress.IPv4Network(item) for item in cidrs)
90
+
91
+ if any(left.overlaps(right) for index, left in enumerate(networks)
92
+ for right in networks[index + 1:]):
93
+ raise StaticSiteError(f"{label} must not contain duplicate or overlapping CIDRs")
94
+
95
+ return cidrs
96
+
97
+
98
+ def _PublicKey(value: object) -> str:
99
+ """Accept one canonical Ed25519 public wire key without comments or options."""
100
+
101
+ if not isinstance(value, str) or not 1 <= len(value) <= 256:
102
+ raise StaticSiteError("admin public_key must be a complete Ed25519 public key")
103
+
104
+ parts = value.split(" ")
105
+
106
+ if len(parts) != 2 or parts[0] != "ssh-ed25519":
107
+ raise StaticSiteError("admin public_key must contain only its type and public blob")
108
+
109
+ try:
110
+ blob = base64.b64decode(parts[1], validate=True)
111
+ expected = struct.pack(">I", 11) + b"ssh-ed25519" + struct.pack(">I", 32)
112
+
113
+ if len(blob) != len(expected) + 32 or not blob.startswith(expected):
114
+ raise ValueError("Invalid public wire key")
115
+
116
+ if base64.b64encode(blob).decode("ascii") != parts[1]:
117
+ raise ValueError("Noncanonical public encoding")
118
+
119
+ except (ValueError, binascii.Error):
120
+ raise StaticSiteError("admin public_key must be a complete Ed25519 public key") from None
121
+
122
+ return value
123
+
124
+
125
+ def _Username(value: object) -> str:
126
+ """Require an explicit portable non-root administrator without command interpolation."""
127
+
128
+ if (not isinstance(value, str) or _USERNAME_PATTERN.fullmatch(value) is None
129
+ or value in {"root", "www-data", "nginx"}):
130
+ raise StaticSiteError("admin username must be portable and separate from root and web accounts")
131
+
132
+ return value
133
+
134
+
135
+ def _Text(value: object, maximum: int, label: str, *, multiline: bool = False) -> str:
136
+ """Accept publishable bounded text, excluding controls and invalid Unicode scalars."""
137
+
138
+ if not isinstance(value, str) or not value.strip():
139
+ raise StaticSiteError(f"{label} must contain nonempty plain text")
140
+
141
+ if any(unicodedata.category(character).startswith("C")
142
+ and not (multiline and character == "\n") for character in value):
143
+ raise StaticSiteError(f"{label} must not contain control characters")
144
+
145
+ if len(value.encode("utf-8")) > maximum:
146
+ raise StaticSiteError(f"{label} must not exceed {maximum} UTF-8 bytes")
147
+
148
+ return value
149
+
150
+
151
+ @dataclass(frozen=True, slots=True)
152
+ class StaticSiteProfile:
153
+ """Explicit Yandex placement and one escaped static page with separate SSH authority."""
154
+
155
+ identity: StackIdentity
156
+ zone_id: str
157
+ image_id: str
158
+ subnet_cidr: str
159
+ admin_username: str
160
+ admin_public_key: str = field(repr=False)
161
+ admin_source_cidrs: tuple[str, ...]
162
+ http_source_cidrs: tuple[str, ...]
163
+ title: str = field(repr=False)
164
+ body: str = field(repr=False)
165
+ cores: int = 2
166
+ memory_gib: int = 2
167
+ boot_disk_gib: int = 20
168
+ schema_version: int = 1
169
+ guest_contract: str = GUEST_CONTRACT
170
+
171
+ def __post_init__(self) -> None:
172
+ """Keep direct construction as strict as the versioned TOML input boundary."""
173
+
174
+ ValidateSchemaVersion(self.schema_version)
175
+
176
+ if not isinstance(self.identity, StackIdentity) or self.identity.provider != "yandex-cloud":
177
+ raise StaticSiteError("identity must explicitly select the supported yandex-cloud provider")
178
+
179
+ if self.guest_contract != GUEST_CONTRACT:
180
+ raise StaticSiteError("guest_contract must select ubuntu-24.04-cloud-init")
181
+
182
+ ValidateIdentifier(self.zone_id, "zone_id")
183
+ ValidateIdentifier(self.image_id, "image_id")
184
+ network = ipaddress.IPv4Network(_Cidr(self.subnet_cidr, 16, "subnet_cidr"))
185
+ private_ranges = tuple(ipaddress.IPv4Network(value) for value in (
186
+ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
187
+ ))
188
+
189
+ if network.prefixlen > 28 or not any(network.subnet_of(item) for item in private_ranges):
190
+ raise StaticSiteError("subnet_cidr must be RFC1918 with prefix /16../28")
191
+
192
+ _Username(self.admin_username)
193
+ _PublicKey(self.admin_public_key)
194
+ _Cidrs(self.admin_source_cidrs, 24, "admin source CIDRs")
195
+ _Cidrs(self.http_source_cidrs, 0, "HTTP source CIDRs")
196
+ _Text(self.title, 160, "site title")
197
+ _Text(self.body, 8192, "site body", multiline=True)
198
+ _Integer(self.cores, 2, 32, "cores")
199
+ _Integer(self.memory_gib, 1, 64, "memory_gib")
200
+ _Integer(self.boot_disk_gib, 10, 256, "boot_disk_gib")
201
+
202
+
203
+ def ParseStaticSiteProfile(data: Mapping[str, object]) -> StaticSiteProfile:
204
+ """Decode a strict concrete profile without accepting commands or private credentials."""
205
+
206
+ try:
207
+ ValidateFields(data, frozenset({
208
+ "schema_version", "profile", "guest_contract", "identity", "placement", "admin", "site",
209
+ }), frozenset(), "static-site profile")
210
+ ValidateSchemaVersion(data["schema_version"])
211
+
212
+ if data["profile"] != PROFILE_NAME or data["guest_contract"] != GUEST_CONTRACT:
213
+ raise StaticSiteError("profile and guest_contract must select the supported static site")
214
+
215
+ placement = RequireTable(data["placement"], "placement")
216
+ admin = RequireTable(data["admin"], "admin")
217
+ site = RequireTable(data["site"], "site")
218
+ ValidateFields(placement, _PLACEMENT_FIELDS, _SIZING_FIELDS, "placement")
219
+ ValidateFields(admin, frozenset({"username", "public_key", "source_cidrs"}),
220
+ frozenset(), "admin")
221
+ ValidateFields(site, frozenset({"title", "body", "http_source_cidrs"}),
222
+ frozenset(), "site")
223
+
224
+ if not isinstance(admin["source_cidrs"], list) or not isinstance(site["http_source_cidrs"], list):
225
+ raise StaticSiteError("Admin and HTTP source CIDRs must be explicit arrays")
226
+
227
+ return StaticSiteProfile(
228
+ identity=ParseIdentity(data["identity"]),
229
+ zone_id=ValidateIdentifier(placement["zone_id"], "zone_id"),
230
+ image_id=ValidateIdentifier(placement["image_id"], "image_id"),
231
+ subnet_cidr=_Cidr(placement["subnet_cidr"], 16, "subnet_cidr"),
232
+ admin_username=_Username(admin["username"]),
233
+ admin_public_key=_PublicKey(admin["public_key"]),
234
+ admin_source_cidrs=tuple(admin["source_cidrs"]),
235
+ http_source_cidrs=tuple(site["http_source_cidrs"]),
236
+ title=_Text(site["title"], 160, "site title"),
237
+ body=_Text(site["body"], 8192, "site body", multiline=True),
238
+ cores=_Integer(placement.get("cores", 2), 2, 32, "cores"),
239
+ memory_gib=_Integer(placement.get("memory_gib", 2), 1, 64, "memory_gib"),
240
+ boot_disk_gib=_Integer(placement.get("boot_disk_gib", 20), 10, 256, "boot_disk_gib"),
241
+ )
242
+
243
+ except ContractError as error:
244
+ raise StaticSiteError(str(error)) from None
245
+
246
+
247
+ def LoadStaticSiteProfile(path: str | Path) -> StaticSiteProfile:
248
+ """Read bounded regular UTF-8 TOML without following symlinks or blocking on special files."""
249
+
250
+ descriptor: int | None = None
251
+
252
+ try:
253
+ profile_path = Path(path).absolute()
254
+
255
+ if ".." in profile_path.parts or any(item.is_symlink() for item in (profile_path, *profile_path.parents)):
256
+ raise StaticSiteError("Profile paths must not contain traversal or symlinks")
257
+
258
+ descriptor = os.open(profile_path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
259
+ | getattr(os, "O_NONBLOCK", 0))
260
+ info = os.fstat(descriptor)
261
+
262
+ if not stat.S_ISREG(info.st_mode) or not 1 <= info.st_size <= MAX_PROFILE_BYTES:
263
+ raise StaticSiteError("Profile input must be a bounded regular file")
264
+
265
+ with os.fdopen(descriptor, "rb") as source:
266
+ descriptor = None
267
+ raw = source.read(MAX_PROFILE_BYTES + 1)
268
+
269
+ if len(raw) > MAX_PROFILE_BYTES:
270
+ raise StaticSiteError("Static-site profile exceeds its bounded input size")
271
+
272
+ data = tomllib.loads(raw.decode("utf-8"))
273
+
274
+ except (OSError, ValueError, RecursionError):
275
+ raise StaticSiteError("Unable to read a valid bounded static-site profile") from None
276
+
277
+ finally:
278
+ if descriptor is not None:
279
+ os.close(descriptor)
280
+
281
+ return ParseStaticSiteProfile(data)
282
+
283
+
284
+ @dataclass(frozen=True, slots=True)
285
+ class StaticSitePlan:
286
+ """Immutable profile intent whose serialized plan binds an exact owned initialization."""
287
+
288
+ profile: StaticSiteProfile
289
+
290
+ def __post_init__(self) -> None:
291
+ """Reject unsupported values before resource or artifact serialization."""
292
+
293
+ if not isinstance(self.profile, StaticSiteProfile):
294
+ raise StaticSiteError("profile must be a StaticSiteProfile")
295
+
296
+
297
+ def CompileStaticSite(profile: StaticSiteProfile) -> StaticSitePlan:
298
+ """Validate composed names without invoking storage, lifecycle, or provider mutation."""
299
+
300
+ plan = StaticSitePlan(profile)
301
+
302
+ for logical_id in ("network", "subnet", "firewall", "address", "boot-disk", "instance", "plan"):
303
+ ValidateName(profile.identity.stack + "-" + logical_id, "resource or bundle name")
304
+
305
+ return plan
306
+
307
+
308
+ def _Page(profile: StaticSiteProfile) -> str:
309
+ """Escape all profile text into fixed markup with no template or script execution."""
310
+
311
+ title = html.escape(profile.title, quote=True)
312
+ body = html.escape(profile.body, quote=True)
313
+
314
+ return "\n".join([
315
+ "<!doctype html>", '<html lang="en">', "<head>", '<meta charset="utf-8">',
316
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
317
+ f"<title>{title}</title>", "</head>", "<body>", f"<h1>{title}</h1>",
318
+ f'<p style="white-space: pre-wrap">{body}</p>', "</body>", "</html>", "",
319
+ ])
320
+
321
+
322
+ def _Firewall(profile: StaticSiteProfile) -> str:
323
+ """Allow declared SSH and HTTP sources while denying routed traffic and other ingress."""
324
+
325
+ lines = [
326
+ "#!/usr/sbin/nft -f", "flush ruleset", "table inet flayer_filter {", " chain input {",
327
+ " type filter hook input priority 0; policy drop;", " ct state invalid drop",
328
+ " ct state established,related accept", ' iifname "lo" accept',
329
+ " udp sport 67 udp dport 68 accept",
330
+ ]
331
+
332
+ for cidr in profile.admin_source_cidrs:
333
+ lines.append(f" ip saddr {cidr} tcp dport 22 accept")
334
+
335
+ for cidr in profile.http_source_cidrs:
336
+ lines.append(f" ip saddr {cidr} tcp dport 80 accept")
337
+
338
+ lines.extend([
339
+ " }", " chain forward {", " type filter hook forward priority 0; policy drop;",
340
+ " }", "}", "",
341
+ ])
342
+
343
+ return "\n".join(lines)
344
+
345
+
346
+ def _CloudInit(profile: StaticSiteProfile) -> bytes:
347
+ """Generate only fixed nonsecret guest operations and escaped publishable page text."""
348
+
349
+ ssh_configuration = "\n".join([
350
+ "Port 22", "UsePAM yes", "PermitRootLogin no", "PasswordAuthentication no",
351
+ "KbdInteractiveAuthentication no", "PubkeyAuthentication yes", "StrictModes yes",
352
+ "AuthorizedKeysFile /etc/ssh/f-layer-static-admin-keys", "AuthorizedKeysCommand none",
353
+ "TrustedUserCAKeys none", "AuthorizedPrincipalsFile none", "AuthenticationMethods publickey",
354
+ "AllowUsers " + " ".join(f"{profile.admin_username}@{cidr}" for cidr in profile.admin_source_cidrs),
355
+ "MaxAuthTries 3", "LoginGraceTime 30", "X11Forwarding no", "AllowAgentForwarding no",
356
+ "AllowTcpForwarding no", "AllowStreamLocalForwarding no", "PermitTunnel no",
357
+ "GatewayPorts no", "PermitUserRC no", "",
358
+ ])
359
+ nginx_configuration = "\n".join([
360
+ "user www-data;", "worker_processes auto;", "pid /run/nginx.pid;",
361
+ "events { worker_connections 128; }", "http {", " default_type text/html;",
362
+ " server_tokens off;", " access_log off;", " server {", " listen 80 default_server;",
363
+ " server_name _;", " root /var/www/f-layer-static;", " autoindex off;",
364
+ " add_header X-Content-Type-Options nosniff always;",
365
+ " add_header Content-Security-Policy \"default-src 'none'; style-src 'unsafe-inline'\" always;",
366
+ " location = / { try_files /index.html =404; }",
367
+ " location = /index.html { try_files /index.html =404; }",
368
+ " location / { return 404; }", " }", "}", "",
369
+ ])
370
+ sysctl_configuration = "\n".join([
371
+ "net.ipv4.ip_forward=0", "net.ipv4.conf.all.accept_redirects=0",
372
+ "net.ipv4.conf.default.accept_redirects=0", "net.ipv4.conf.all.send_redirects=0",
373
+ "net.ipv4.conf.default.send_redirects=0", "net.ipv4.conf.all.accept_source_route=0",
374
+ "net.ipv4.conf.default.accept_source_route=0", "net.ipv6.conf.all.disable_ipv6=1",
375
+ "net.ipv6.conf.default.disable_ipv6=1", "",
376
+ ])
377
+ bootstrap = "\n".join([
378
+ "#!/bin/sh", "set -eu", "install -d -m 0755 /run/sshd",
379
+ "chown root:root /etc/ssh/f-layer-static-admin-keys",
380
+ "chmod 0644 /etc/ssh/f-layer-static-admin-keys",
381
+ "chown root:root /var/www/f-layer-static /var/www/f-layer-static/index.html",
382
+ "chmod 0755 /var/www/f-layer-static", "chmod 0644 /var/www/f-layer-static/index.html",
383
+ "sshd -t", "nft --check --file /etc/nftables.conf", "nginx -t -c /etc/nginx/nginx.conf",
384
+ "sysctl --system", "systemctl enable --now nftables",
385
+ "systemctl enable --now unattended-upgrades", "systemctl disable --now ssh.socket",
386
+ "systemctl daemon-reload", "systemctl enable --now ssh.service",
387
+ "systemctl restart ssh.service", "systemctl enable --now nginx.service",
388
+ "systemctl restart nginx.service", "",
389
+ ])
390
+ files = [
391
+ ("/etc/ssh/sshd_config", "0644", ssh_configuration),
392
+ ("/etc/ssh/f-layer-static-admin-keys", "0644", profile.admin_public_key + "\n"),
393
+ ("/etc/nginx/nginx.conf", "0644", nginx_configuration),
394
+ ("/var/www/f-layer-static/index.html", "0644", _Page(profile)),
395
+ ("/etc/sysctl.d/90-f-layer-static.conf", "0644", sysctl_configuration),
396
+ ("/etc/nftables.conf", "0644", _Firewall(profile)),
397
+ ("/usr/local/sbin/f-layer-static-bootstrap", "0700", bootstrap),
398
+ ]
399
+ data = {
400
+ "ssh_pwauth": False, "disable_root": True,
401
+ "users": [{
402
+ "name": profile.admin_username, "groups": ["sudo"], "shell": "/bin/bash",
403
+ "lock_passwd": True, "sudo": "ALL=(ALL) NOPASSWD:ALL",
404
+ }],
405
+ "package_update": True, "package_upgrade": False,
406
+ "packages": ["nginx", "nftables", "unattended-upgrades"],
407
+ "write_files": [{
408
+ "path": path, "owner": "root:root", "permissions": permissions, "content": content,
409
+ } for path, permissions, content in files],
410
+ "runcmd": [["/bin/sh", "/usr/local/sbin/f-layer-static-bootstrap"]],
411
+ }
412
+
413
+ return ("#cloud-config\n" + json.dumps(data, indent=2, sort_keys=True) + "\n").encode("utf-8")
414
+
415
+
416
+ def BuildStaticSiteBundle(profile: StaticSiteProfile) -> ArtifactBundle:
417
+ """Create public initialization bytes without credentials or guest readiness claims."""
418
+
419
+ CompileStaticSite(profile)
420
+
421
+ return ArtifactBundle(profile.identity, "server", profile.identity.stack, (
422
+ Artifact(CLOUD_INIT_FILENAME, _CloudInit(profile), sensitive=False),
423
+ ))
424
+
425
+
426
+ def RenderStaticSitePlan(plan: StaticSitePlan, *, user_data_file: str | Path) -> str:
427
+ """Bind the existing six-resource lifecycle contract to verified owned initialization."""
428
+
429
+ if not isinstance(plan, StaticSitePlan):
430
+ raise StaticSiteError("plan must be a StaticSitePlan")
431
+
432
+ profile = plan.profile
433
+ artifact_path = Path(user_data_file)
434
+ expected = _CloudInit(profile)
435
+
436
+ if artifact_path.name != CLOUD_INIT_FILENAME:
437
+ raise StaticSiteError("Initialization must reference the exact static-site artifact filename")
438
+
439
+ try:
440
+ actual = ReadOwnedArtifactFile(artifact_path, profile.identity, kind="server",
441
+ name=profile.identity.stack)
442
+
443
+ except ContractError:
444
+ raise StaticSiteError("Prepared static-site artifact ownership or integrity is invalid") from None
445
+
446
+ if actual != expected:
447
+ raise StaticSiteError("Initialization artifact does not match the compiled static-site profile")
448
+
449
+ def Quote(value: str) -> str:
450
+ """Serialize Unicode scalar strings as valid TOML without path expansion."""
451
+
452
+ return json.dumps(value, ensure_ascii=False)
453
+
454
+ lines = ["schema_version = 1", f"profile = {Quote(PROFILE_NAME)}", "", "[identity]"]
455
+
456
+ for key in sorted(IDENTITY_FIELDS):
457
+ lines.append(f"{key} = {Quote(getattr(profile.identity, key))}")
458
+
459
+ def AddResource(
460
+ logical_id: str, kind: str, dependencies: tuple[str, ...], parameters: Mapping[str, object],
461
+ ) -> None:
462
+ """Render deterministic scalar resource intent without observed cloud identifiers."""
463
+
464
+ lines.extend([
465
+ "", "[[resources]]", f"logical_id = {Quote(logical_id)}", f"kind = {Quote(kind)}",
466
+ f"name = {Quote(ValidateName(profile.identity.stack + '-' + logical_id, 'resource name'))}",
467
+ "dependencies = [" + ", ".join(Quote(item) for item in dependencies) + "]",
468
+ "[resources.parameters]",
469
+ ])
470
+
471
+ for key, value in parameters.items():
472
+ if isinstance(value, str):
473
+ rendered = Quote(value)
474
+
475
+ elif type(value) is int:
476
+ rendered = str(value)
477
+
478
+ else:
479
+ raise StaticSiteError("Resource parameters must be explicit scalar values")
480
+
481
+ lines.append(f"{key} = {rendered}")
482
+
483
+ AddResource("network", "network", (), {})
484
+ AddResource("subnet", "subnet", ("network",), {
485
+ "zone_id": profile.zone_id, "ipv4_cidr": profile.subnet_cidr,
486
+ "network_dependency": "network",
487
+ })
488
+ AddResource("firewall", "security-group", ("network",), {"network_dependency": "network"})
489
+
490
+ for port, cidrs in ((22, profile.admin_source_cidrs), (80, profile.http_source_cidrs)):
491
+ for cidr in cidrs:
492
+ lines.extend([
493
+ "[[resources.parameters.rules]]", 'direction = "ingress"', 'protocol = "tcp"',
494
+ f"from_port = {port}", f"to_port = {port}", f"cidr = {Quote(cidr)}",
495
+ ])
496
+
497
+ lines.extend([
498
+ "[[resources.parameters.rules]]", 'direction = "egress"', 'protocol = "any"',
499
+ 'cidr = "0.0.0.0/0"',
500
+ ])
501
+ AddResource("address", "address", (), {"zone_id": profile.zone_id})
502
+ AddResource("boot-disk", "disk", (), {
503
+ "zone_id": profile.zone_id, "image_id": profile.image_id, "size_gib": profile.boot_disk_gib,
504
+ })
505
+ AddResource("instance", "instance", ("subnet", "firewall", "address", "boot-disk"), {
506
+ "zone_id": profile.zone_id, "cores": profile.cores, "memory_gib": profile.memory_gib,
507
+ "boot_disk_dependency": "boot-disk", "subnet_dependency": "subnet",
508
+ "security_group_dependency": "firewall", "address_dependency": "address",
509
+ "ssh_public_key": profile.admin_public_key, "ssh_username": profile.admin_username,
510
+ "user_data_file": str(artifact_path.absolute()),
511
+ "user_data_sha256": hashlib.sha256(expected).hexdigest(),
512
+ })
513
+
514
+ return "\n".join(lines) + "\n"
515
+
516
+
517
+ @dataclass(frozen=True, slots=True)
518
+ class StaticSitePreparation:
519
+ """Local bundle references for a prepared plan; deployment and readiness remain unverified."""
520
+
521
+ server_directory: Path
522
+ plan_directory: Path
523
+
524
+ @property
525
+ def PlanFile(self) -> Path:
526
+ """Locate the prepared TOML inside its separately owned private plan bundle."""
527
+
528
+ return self.plan_directory / PLAN_FILENAME
529
+
530
+
531
+ def PrepareStaticSite(profile: StaticSiteProfile, *, artifact_root: str | Path) -> StaticSitePreparation:
532
+ """Publish exclusive owned bundles; preserve a verified plan after publication failure."""
533
+
534
+ plan = CompileStaticSite(profile)
535
+ server_bundle = BuildStaticSiteBundle(profile)
536
+ server_directory = WriteArtifactBundle(artifact_root, server_bundle)
537
+ plan_text: str | None = None
538
+
539
+ try:
540
+ plan_text = RenderStaticSitePlan(plan, user_data_file=server_directory / CLOUD_INIT_FILENAME)
541
+ plan_directory = WriteArtifactBundle(artifact_root, ArtifactBundle(
542
+ profile.identity, "server", profile.identity.stack + "-plan", (
543
+ Artifact(PLAN_FILENAME, plan_text.encode("utf-8"), sensitive=False),
544
+ ),
545
+ ))
546
+
547
+ except BaseException:
548
+ published_plan = False
549
+
550
+ if plan_text is not None:
551
+ try:
552
+ actual = ReadOwnedArtifactFile(
553
+ Path(artifact_root) / ("server-" + profile.identity.stack + "-plan") / PLAN_FILENAME,
554
+ profile.identity, kind="server", name=profile.identity.stack + "-plan",
555
+ )
556
+ published_plan = actual == plan_text.encode("utf-8")
557
+
558
+ except ContractError:
559
+ pass
560
+
561
+ if not published_plan:
562
+ RemoveArtifactBundle(
563
+ artifact_root, profile.identity, kind="server", name=profile.identity.stack,
564
+ expected_bundle=server_bundle,
565
+ )
566
+
567
+ raise
568
+
569
+ return StaticSitePreparation(server_directory, plan_directory)
570
+
571
+
572
+ def Main(argv: Sequence[str] | None = None) -> int:
573
+ """Prepare bounded local artifacts only; never instantiate or invoke a cloud provider."""
574
+
575
+ parser = argparse.ArgumentParser(description="Prepare an owned static-site lifecycle plan locally")
576
+ parser.add_argument("profile_file", help="Explicit version-one static-site TOML profile")
577
+ parser.add_argument("--artifact-root", required=True, help="Private POSIX artifact root")
578
+ arguments = parser.parse_args(argv)
579
+
580
+ try:
581
+ prepared = PrepareStaticSite(LoadStaticSiteProfile(arguments.profile_file),
582
+ artifact_root=arguments.artifact_root)
583
+
584
+ except ContractError:
585
+ print(json.dumps({"status": "failed", "reason": "Profile or owned artifact preparation failed"}))
586
+
587
+ return 1
588
+
589
+ print(json.dumps({"status": "prepared-unverified", "plan_file": str(prepared.PlanFile.absolute())},
590
+ sort_keys=True))
591
+
592
+ return 0
593
+
594
+
595
+ if __name__ == "__main__":
596
+ raise SystemExit(Main())