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,609 @@
1
+ """Bounded artifact bundles with exclusive ownership and verified local cleanup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ from contextlib import contextmanager
11
+ from dataclasses import asdict, dataclass, field
12
+ from pathlib import Path
13
+ from typing import Iterator
14
+
15
+ from flayer.core.contracts import (
16
+ ContractError,
17
+ ParseIdentity,
18
+ RequireTable,
19
+ StackIdentity,
20
+ ValidateFields,
21
+ ValidateName,
22
+ )
23
+
24
+ MAX_ARTIFACT_BYTES = 1048576
25
+ MAX_BUNDLE_BYTES = 2097152
26
+ MAX_MANIFEST_BYTES = 16384
27
+ MAX_BUNDLE_FILES = 8
28
+ _FILE_PATTERN = re.compile(r"[a-z][a-z0-9-]{0,62}\.[a-z0-9]{1,12}\Z")
29
+ _MANIFEST_NAME = "manifest.json"
30
+
31
+
32
+ class ArtifactError(ContractError):
33
+ """An artifact is invalid, foreign, changed, busy, or unavailable for safe storage."""
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class _FileReceipt:
38
+ """Pinned file identity and exact bytes authorized for a subsequent destructive step."""
39
+
40
+ device: int
41
+ inode: int
42
+ size: int
43
+ modified_ns: int
44
+ changed_ns: int
45
+ sha256: str
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class _OwnedFile:
50
+ """One verified immutable file snapshot excluding its payload from representation."""
51
+
52
+ name: str
53
+ content: bytes = field(repr=False)
54
+ receipt: _FileReceipt
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class _BundleSnapshot:
59
+ """A complete authorized bundle with the manifest and each payload pinned separately."""
60
+
61
+ files: tuple[_OwnedFile, ...]
62
+ manifest: _FileReceipt
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class Artifact:
67
+ """Memory-only generated bytes; sensitive payloads are never included in repr."""
68
+
69
+ name: str
70
+ content: bytes = field(repr=False)
71
+ sensitive: bool = True
72
+
73
+ def __post_init__(self) -> None:
74
+ """Require a portable single filename and a bounded immutable payload."""
75
+
76
+ if (
77
+ not isinstance(self.name, str)
78
+ or _FILE_PATTERN.fullmatch(self.name) is None
79
+ or self.name == _MANIFEST_NAME
80
+ ):
81
+ raise ArtifactError("Artifact filename must be portable and must not be manifest.json")
82
+
83
+ if not isinstance(self.content, bytes) or not 1 <= len(self.content) <= MAX_ARTIFACT_BYTES:
84
+ raise ArtifactError("Artifact content must contain 1..1048576 immutable bytes")
85
+
86
+ if type(self.sensitive) is not bool:
87
+ raise ArtifactError("Artifact sensitivity must be an explicit boolean")
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class ArtifactBundle:
92
+ """A named set owned by one complete stack identity, independent of provider state."""
93
+
94
+ identity: StackIdentity
95
+ kind: str
96
+ name: str
97
+ files: tuple[Artifact, ...] = field(repr=False)
98
+
99
+ def __post_init__(self) -> None:
100
+ """Reject ambiguous bundle names, duplicate files, and oversized compositions."""
101
+
102
+ if not isinstance(self.identity, StackIdentity):
103
+ raise ArtifactError("Artifact identity must be a StackIdentity")
104
+
105
+ if not isinstance(self.kind, str) or self.kind not in {"server", "device"}:
106
+ raise ArtifactError("Artifact kind must be server or device")
107
+
108
+ ValidateName(self.name, "artifact name")
109
+
110
+ if (
111
+ not isinstance(self.files, tuple)
112
+ or not 1 <= len(self.files) <= MAX_BUNDLE_FILES
113
+ or any(not isinstance(item, Artifact) for item in self.files)
114
+ ):
115
+ raise ArtifactError("A bundle must contain 1..8 immutable Artifact values")
116
+
117
+ if len({item.name for item in self.files}) != len(self.files):
118
+ raise ArtifactError("Artifact filenames must be unique")
119
+
120
+ if sum(len(item.content) for item in self.files) > MAX_BUNDLE_BYTES:
121
+ raise ArtifactError("A bundle must not exceed 2097152 bytes")
122
+
123
+
124
+ def BuildDeviceBundle(
125
+ identity: StackIdentity, device_id: str, transport: str, files: tuple[Artifact, ...],
126
+ ) -> ArtifactBundle:
127
+ """Wrap externally issued transport files without generating or validating credentials."""
128
+
129
+ ValidateName(transport, "transport")
130
+ ValidateName(device_id, "device_id")
131
+
132
+ if not isinstance(files, tuple) or not 1 <= len(files) < MAX_BUNDLE_FILES or any(
133
+ not isinstance(item, Artifact) for item in files
134
+ ):
135
+ raise ArtifactError("Device files must contain 1..7 immutable Artifact values")
136
+
137
+ if any(not item.sensitive for item in files):
138
+ raise ArtifactError("Externally issued device profiles must be marked sensitive")
139
+
140
+ descriptor = Artifact("device-info.json", (json.dumps({
141
+ "schema_version": 1, "transport": transport, "device_id": device_id,
142
+ "status": "externally-issued-unverified",
143
+ }, sort_keys=True) + "\n").encode("utf-8"), sensitive=False)
144
+
145
+ return ArtifactBundle(identity, "device", device_id, files + (descriptor,))
146
+
147
+
148
+ def _ValidatePath(path: Path) -> None:
149
+ """Reject parent traversal and preexisting symlink components before opening handles."""
150
+
151
+ if not path.name or ".." in path.parts:
152
+ raise ArtifactError("Artifact storage must name an explicit path without traversal")
153
+
154
+ if any(item.is_symlink() for item in (path, *path.parents)):
155
+ raise ArtifactError("Artifact paths must not contain symlinks")
156
+
157
+
158
+ def _RequireOwned(info: os.stat_result, *, directory: bool) -> None:
159
+ """Require private POSIX ownership and reject special files on every platform."""
160
+
161
+ expected_type = stat.S_ISDIR if directory else stat.S_ISREG
162
+
163
+ if not expected_type(info.st_mode):
164
+ raise ArtifactError("Artifact storage requires regular files and directories")
165
+
166
+ if not directory and info.st_nlink != 1:
167
+ raise ArtifactError("Artifact storage must not contain hardlinked files")
168
+
169
+ if os.name == "posix":
170
+ if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != (0o700 if directory else 0o600):
171
+ raise ArtifactError("Artifact storage must be caller-owned and private")
172
+
173
+
174
+ @contextmanager
175
+ def _Directory(path: Path, *, create: bool = False) -> Iterator[int]:
176
+ """Pin one private directory handle without recursively creating parent paths."""
177
+
178
+ if os.name != "posix":
179
+ raise ArtifactError("Private artifact storage currently requires a POSIX host")
180
+
181
+ _ValidatePath(path)
182
+ descriptor: int | None = None
183
+
184
+ try:
185
+ if create:
186
+ path.mkdir(mode=0o700, exist_ok=True)
187
+
188
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0))
189
+ _RequireOwned(os.fstat(descriptor), directory=True)
190
+
191
+ yield descriptor
192
+
193
+ finally:
194
+ if descriptor is not None:
195
+ os.close(descriptor)
196
+
197
+
198
+ def _StatKey(info: os.stat_result) -> tuple[int, int, int, int, int]:
199
+ """Identify replacements and in-place modifications without treating access time as a write."""
200
+
201
+ return info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns
202
+
203
+
204
+ def _Receipt(info: os.stat_result, content: bytes) -> _FileReceipt:
205
+ """Bind a pinned inode to exact authorized bytes and modification metadata."""
206
+
207
+ return _FileReceipt(*_StatKey(info), hashlib.sha256(content).hexdigest())
208
+
209
+
210
+ def _ReadSnapshot(directory: int, name: str, limit: int) -> tuple[bytes, _FileReceipt]:
211
+ """Read bounded private bytes and reject a file that changes during the read."""
212
+
213
+ descriptor = os.open(
214
+ name, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0),
215
+ dir_fd=directory,
216
+ )
217
+
218
+ try:
219
+ before = os.fstat(descriptor)
220
+ _RequireOwned(before, directory=False)
221
+ result = bytearray()
222
+
223
+ while len(result) <= limit:
224
+ chunk = os.read(descriptor, min(65536, limit + 1 - len(result)))
225
+
226
+ if not chunk:
227
+ break
228
+
229
+ result.extend(chunk)
230
+
231
+ if len(result) > limit:
232
+ raise ArtifactError("Artifact file exceeds its bounded size")
233
+
234
+ after = os.fstat(descriptor)
235
+ _RequireOwned(after, directory=False)
236
+
237
+ if _StatKey(before) != _StatKey(after):
238
+ raise ArtifactError("Artifact changed while its content was being verified")
239
+
240
+ content = bytes(result)
241
+
242
+ return content, _Receipt(after, content)
243
+
244
+ finally:
245
+ os.close(descriptor)
246
+
247
+
248
+ def _ReadFile(directory: int, name: str, limit: int) -> bytes:
249
+ """Return one verified private file snapshot without discarding its bounded-read checks."""
250
+
251
+ return _ReadSnapshot(directory, name, limit)[0]
252
+
253
+
254
+ def _AssertFile(directory: int, name: str, expected: _FileReceipt) -> None:
255
+ """Require the original inode, private ownership, metadata, and exact content before cleanup."""
256
+
257
+ _, actual = _ReadSnapshot(directory, name, MAX_ARTIFACT_BYTES)
258
+
259
+ if actual != expected:
260
+ raise ArtifactError("Artifact was replaced or changed; cleanup is refused")
261
+
262
+
263
+ def _UnlinkFile(directory: int, name: str, expected: _FileReceipt) -> None:
264
+ """Recheck a pinned receipt immediately before unlink; preserve detected replacements."""
265
+
266
+ _AssertFile(directory, name, expected)
267
+ info = os.stat(name, dir_fd=directory, follow_symlinks=False)
268
+ _RequireOwned(info, directory=False)
269
+
270
+ if _StatKey(info) != (
271
+ expected.device, expected.inode, expected.size, expected.modified_ns, expected.changed_ns,
272
+ ):
273
+ raise ArtifactError("Artifact changed before cleanup; removal is refused")
274
+
275
+ os.unlink(name, dir_fd=directory)
276
+
277
+
278
+ def _AssertDirectory(parent: int, name: str, descriptor: int) -> None:
279
+ """Preserve a replacement directory instead of confusing its name with the pinned bundle."""
280
+
281
+ pinned = os.fstat(descriptor)
282
+ named = os.stat(name, dir_fd=parent, follow_symlinks=False)
283
+ _RequireOwned(pinned, directory=True)
284
+ _RequireOwned(named, directory=True)
285
+
286
+ if (pinned.st_dev, pinned.st_ino) != (named.st_dev, named.st_ino):
287
+ raise ArtifactError("Artifact directory was replaced; cleanup is refused")
288
+
289
+
290
+ def ReadArtifactFile(path: str | Path) -> bytes:
291
+ """Read only a private artifact; never expose filesystem paths or bytes in failures."""
292
+
293
+ artifact_path = Path(path)
294
+
295
+ try:
296
+ _ValidatePath(artifact_path)
297
+
298
+ with _Directory(artifact_path.parent) as directory:
299
+ return _ReadFile(directory, artifact_path.name, MAX_ARTIFACT_BYTES)
300
+
301
+ except (OSError, ValueError, RecursionError):
302
+ raise ArtifactError("Unable to read a valid private artifact") from None
303
+
304
+
305
+ def _Manifest(bundle: ArtifactBundle) -> bytes:
306
+ """Record exact ownership and content digests while excluding payloads and credentials."""
307
+
308
+ data = {
309
+ "schema_version": 1, "identity": asdict(bundle.identity), "kind": bundle.kind,
310
+ "name": bundle.name, "files": [{
311
+ "name": item.name, "size": len(item.content), "sensitive": item.sensitive,
312
+ "sha256": hashlib.sha256(item.content).hexdigest(),
313
+ } for item in bundle.files],
314
+ }
315
+
316
+ return (json.dumps(data, indent=2, sort_keys=True) + "\n").encode("utf-8")
317
+
318
+
319
+ def _WriteFile(directory: int, name: str, content: bytes) -> _FileReceipt:
320
+ """Create one exclusive private file and fsync all bytes before returning."""
321
+
322
+ descriptor = os.open(
323
+ name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
324
+ 0o600, dir_fd=directory,
325
+ )
326
+
327
+ written_count = 0
328
+
329
+ try:
330
+ remaining = memoryview(content)
331
+
332
+ while remaining:
333
+ written = os.write(descriptor, remaining)
334
+
335
+ if written <= 0:
336
+ raise OSError("Incomplete artifact write")
337
+
338
+ remaining = remaining[written:]
339
+ written_count += written
340
+
341
+ os.fsync(descriptor)
342
+
343
+ except BaseException:
344
+ try:
345
+ receipt = _Receipt(os.fstat(descriptor), content[:written_count])
346
+
347
+ finally:
348
+ os.close(descriptor)
349
+
350
+ _UnlinkFile(directory, name, receipt)
351
+
352
+ raise
353
+
354
+ else:
355
+ try:
356
+ receipt = _Receipt(os.fstat(descriptor), content)
357
+
358
+ finally:
359
+ os.close(descriptor)
360
+
361
+ return receipt
362
+
363
+
364
+ @contextmanager
365
+ def _BundleLock(directory: int, name: str) -> Iterator[None]:
366
+ """Serialize cooperating operations without deleting an existing crash or foreign lock."""
367
+
368
+ lock_name = f".{name}.lock"
369
+ descriptor = os.open(lock_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600, dir_fd=directory)
370
+
371
+ try:
372
+ receipt = _Receipt(os.fstat(descriptor), b"")
373
+
374
+ finally:
375
+ os.close(descriptor)
376
+
377
+ try:
378
+ yield
379
+
380
+ finally:
381
+ _UnlinkFile(directory, lock_name, receipt)
382
+
383
+
384
+ def WriteArtifactBundle(root: str | Path, bundle: ArtifactBundle) -> Path:
385
+ """Publish a new manifest-last bundle exclusively and roll back only created files."""
386
+
387
+ if not isinstance(bundle, ArtifactBundle):
388
+ raise ArtifactError("bundle must be an ArtifactBundle")
389
+
390
+ root_path = Path(root)
391
+ bundle_name = f"{bundle.kind}-{bundle.name}"
392
+ created: list[tuple[str, _FileReceipt]] = []
393
+ directory: int | None = None
394
+
395
+ try:
396
+ with _Directory(root_path, create=True) as parent:
397
+ with _BundleLock(parent, bundle_name):
398
+ try:
399
+ os.mkdir(bundle_name, 0o700, dir_fd=parent)
400
+ directory = os.open(bundle_name, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0), dir_fd=parent)
401
+
402
+ for artifact in bundle.files:
403
+ _AssertDirectory(parent, bundle_name, directory)
404
+ receipt = _WriteFile(directory, artifact.name, artifact.content)
405
+ created.append((artifact.name, receipt))
406
+
407
+ for name, receipt in created:
408
+ _AssertFile(directory, name, receipt)
409
+
410
+ _AssertDirectory(parent, bundle_name, directory)
411
+ receipt = _WriteFile(directory, _MANIFEST_NAME, _Manifest(bundle))
412
+ created.append((_MANIFEST_NAME, receipt))
413
+ os.fsync(directory)
414
+ os.fsync(parent)
415
+
416
+ except BaseException:
417
+ if directory is not None:
418
+ _AssertDirectory(parent, bundle_name, directory)
419
+
420
+ for name, receipt in reversed(created):
421
+ _UnlinkFile(directory, name, receipt)
422
+
423
+ _AssertDirectory(parent, bundle_name, directory)
424
+ os.rmdir(bundle_name, dir_fd=parent)
425
+
426
+ raise
427
+
428
+ finally:
429
+ if directory is not None:
430
+ os.close(directory)
431
+
432
+ except (OSError, ValueError, RecursionError):
433
+ raise ArtifactError("Unable to publish artifact bundle; existing bundles are never replaced") from None
434
+
435
+ return root_path / bundle_name
436
+
437
+
438
+ def _UniqueObject(pairs: list[tuple[str, object]]) -> dict[str, object]:
439
+ """Reject duplicate JSON fields at the untrusted ownership-manifest boundary."""
440
+
441
+ data: dict[str, object] = {}
442
+
443
+ for key, value in pairs:
444
+ if key in data:
445
+ raise ArtifactError("Artifact manifest contains duplicate fields")
446
+
447
+ data[key] = value
448
+
449
+ return data
450
+
451
+
452
+ def _ValidateManifest(
453
+ directory: int, identity: StackIdentity, kind: str, name: str,
454
+ ) -> _BundleSnapshot:
455
+ """Check the complete manifest and every file before authorizing any removal."""
456
+
457
+ raw, manifest_receipt = _ReadSnapshot(directory, _MANIFEST_NAME, MAX_MANIFEST_BYTES)
458
+ data = RequireTable(json.loads(raw, object_pairs_hook=_UniqueObject), "artifact manifest")
459
+ ValidateFields(data, frozenset({"schema_version", "identity", "kind", "name", "files"}), frozenset(), "artifact manifest")
460
+
461
+ if type(data["schema_version"]) is not int or data["schema_version"] != 1:
462
+ raise ArtifactError("Artifact manifest schema is unsupported")
463
+
464
+ if ParseIdentity(data["identity"]) != identity or data["kind"] != kind or data["name"] != name:
465
+ raise ArtifactError("Artifact manifest does not match the expected ownership")
466
+
467
+ files = data["files"]
468
+
469
+ if not isinstance(files, list) or not 1 <= len(files) <= MAX_BUNDLE_FILES:
470
+ raise ArtifactError("Artifact manifest must contain 1..8 file entries")
471
+
472
+ names: list[str] = []
473
+ snapshots: list[_OwnedFile] = []
474
+ total_size = 0
475
+
476
+ for raw_file in files:
477
+ item = RequireTable(raw_file, "artifact entry")
478
+ ValidateFields(item, frozenset({"name", "size", "sha256", "sensitive"}), frozenset(), "artifact entry")
479
+ filename, size, digest = item["name"], item["size"], item["sha256"]
480
+
481
+ if not isinstance(filename, str) or _FILE_PATTERN.fullmatch(filename) is None or filename == _MANIFEST_NAME:
482
+ raise ArtifactError("Artifact manifest contains an unsafe filename")
483
+
484
+ if type(size) is not int or not 1 <= size <= MAX_ARTIFACT_BYTES or type(item["sensitive"]) is not bool:
485
+ raise ArtifactError("Artifact manifest contains an invalid size or sensitivity")
486
+
487
+ if not isinstance(digest, str) or re.fullmatch(r"[a-f0-9]{64}", digest) is None:
488
+ raise ArtifactError("Artifact manifest contains an invalid digest")
489
+
490
+ content, receipt = _ReadSnapshot(directory, filename, MAX_ARTIFACT_BYTES)
491
+
492
+ if len(content) != size or hashlib.sha256(content).hexdigest() != digest:
493
+ raise ArtifactError("Artifact content differs from its owned manifest")
494
+
495
+ names.append(filename)
496
+ snapshots.append(_OwnedFile(filename, content, receipt))
497
+ total_size += size
498
+
499
+ if len(set(names)) != len(names) or total_size > MAX_BUNDLE_BYTES:
500
+ raise ArtifactError("Artifact manifest has duplicates or exceeds its total size limit")
501
+
502
+ if set(os.listdir(directory)) != {*names, _MANIFEST_NAME}:
503
+ raise ArtifactError("Artifact bundle contains unowned files; cleanup is refused")
504
+
505
+ return _BundleSnapshot(tuple(snapshots), manifest_receipt)
506
+
507
+
508
+ def ReadOwnedArtifactFile(
509
+ path: str | Path, identity: StackIdentity, *, kind: str, name: str,
510
+ ) -> bytes:
511
+ """Authorize one exact file through the complete unchanged bundle ownership manifest."""
512
+
513
+ if not isinstance(identity, StackIdentity) or not isinstance(kind, str) or kind not in {"server", "device"}:
514
+ raise ArtifactError("Artifact read requires explicit identity and bundle kind")
515
+
516
+ ValidateName(name, "artifact name")
517
+ artifact_path = Path(path)
518
+
519
+ if artifact_path.parent.name != f"{kind}-{name}":
520
+ raise ArtifactError("Artifact path does not match the expected bundle name")
521
+
522
+ try:
523
+ _ValidatePath(artifact_path)
524
+
525
+ with _Directory(artifact_path.parent) as directory:
526
+ snapshot = _ValidateManifest(directory, identity, kind, name)
527
+ file = next((item for item in snapshot.files if item.name == artifact_path.name), None)
528
+
529
+ if file is None:
530
+ raise ArtifactError("Artifact filename is not owned by its bundle manifest")
531
+
532
+ return file.content
533
+
534
+ except (OSError, ValueError, RecursionError):
535
+ raise ArtifactError("Unable to read an unchanged owned artifact bundle") from None
536
+
537
+
538
+ def RemoveArtifactBundle(
539
+ root: str | Path, identity: StackIdentity, *, kind: str, name: str,
540
+ expected_bundle: ArtifactBundle | None = None,
541
+ ) -> bool:
542
+ """Remove unchanged owned files, optionally binding cleanup to exact caller-expected content."""
543
+
544
+ if not isinstance(identity, StackIdentity) or not isinstance(kind, str) or kind not in {"server", "device"}:
545
+ raise ArtifactError("Artifact removal requires explicit identity and bundle kind")
546
+
547
+ ValidateName(name, "artifact name")
548
+
549
+ if expected_bundle is not None and (
550
+ not isinstance(expected_bundle, ArtifactBundle)
551
+ or expected_bundle.identity != identity or expected_bundle.kind != kind
552
+ or expected_bundle.name != name
553
+ ):
554
+ raise ArtifactError("Expected cleanup bundle must match the complete requested ownership")
555
+
556
+ root_path = Path(root)
557
+ bundle_name = f"{kind}-{name}"
558
+
559
+ try:
560
+ _ValidatePath(root_path)
561
+
562
+ if not root_path.exists():
563
+ return False
564
+
565
+ with _Directory(root_path) as parent:
566
+ with _BundleLock(parent, bundle_name):
567
+ try:
568
+ descriptor = os.open(bundle_name, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0), dir_fd=parent)
569
+
570
+ except FileNotFoundError:
571
+ return False
572
+
573
+ try:
574
+ _RequireOwned(os.fstat(descriptor), directory=True)
575
+ snapshot = _ValidateManifest(descriptor, identity, kind, name)
576
+
577
+ if expected_bundle is not None and (
578
+ {item.name: item.content for item in snapshot.files}
579
+ != {item.name: item.content for item in expected_bundle.files}
580
+ or snapshot.manifest.sha256
581
+ != hashlib.sha256(_Manifest(expected_bundle)).hexdigest()
582
+ ):
583
+ raise ArtifactError("Artifact bundle differs from the expected cleanup content")
584
+
585
+ _AssertDirectory(parent, bundle_name, descriptor)
586
+
587
+ for file in snapshot.files:
588
+ _AssertFile(descriptor, file.name, file.receipt)
589
+
590
+ _AssertFile(descriptor, _MANIFEST_NAME, snapshot.manifest)
591
+
592
+ for file in snapshot.files:
593
+ _AssertDirectory(parent, bundle_name, descriptor)
594
+ _UnlinkFile(descriptor, file.name, file.receipt)
595
+
596
+ _UnlinkFile(descriptor, _MANIFEST_NAME, snapshot.manifest)
597
+ os.fsync(descriptor)
598
+ _AssertDirectory(parent, bundle_name, descriptor)
599
+ os.rmdir(bundle_name, dir_fd=parent)
600
+
601
+ finally:
602
+ os.close(descriptor)
603
+
604
+ os.fsync(parent)
605
+
606
+ except (OSError, ValueError, RecursionError):
607
+ raise ArtifactError("Unable to remove a valid owned artifact bundle; partial cleanup may require recovery") from None
608
+
609
+ return True