verdog-runtime 0.1.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.
- verdog_runtime/__init__.py +13 -0
- verdog_runtime/_artifact_references.py +548 -0
- verdog_runtime/_checkpoint_compatibility.py +172 -0
- verdog_runtime/_child_checkpoint.py +262 -0
- verdog_runtime/_configuration.py +230 -0
- verdog_runtime/_definitions.py +45 -0
- verdog_runtime/_encoding.py +13 -0
- verdog_runtime/_file_lock.py +79 -0
- verdog_runtime/_lifecycle.py +171 -0
- verdog_runtime/_markdown.py +29 -0
- verdog_runtime/_process.py +211 -0
- verdog_runtime/_protocol.py +494 -0
- verdog_runtime/_relative_path.py +21 -0
- verdog_runtime/_run_metadata.py +989 -0
- verdog_runtime/_run_model.py +263 -0
- verdog_runtime/_run_store.py +682 -0
- verdog_runtime/_statistics.py +294 -0
- verdog_runtime/agents/__init__.py +31 -0
- verdog_runtime/agents/_artifacts.py +84 -0
- verdog_runtime/agents/_command.py +197 -0
- verdog_runtime/agents/claude.py +148 -0
- verdog_runtime/agents/codex.py +164 -0
- verdog_runtime/cancellation.py +69 -0
- verdog_runtime/child.py +979 -0
- verdog_runtime/cli.py +180 -0
- verdog_runtime/declarations/__init__.py +137 -0
- verdog_runtime/declarations/agents.py +61 -0
- verdog_runtime/declarations/calls.py +65 -0
- verdog_runtime/declarations/configuration.py +18 -0
- verdog_runtime/declarations/context.py +88 -0
- verdog_runtime/declarations/graph.py +262 -0
- verdog_runtime/declarations/ids.py +36 -0
- verdog_runtime/declarations/interfaces.py +12 -0
- verdog_runtime/declarations/keys.py +20 -0
- verdog_runtime/declarations/operations.py +103 -0
- verdog_runtime/declarations/results.py +63 -0
- verdog_runtime/declarations/state.py +179 -0
- verdog_runtime/entry.py +563 -0
- verdog_runtime/interpreter/__init__.py +47 -0
- verdog_runtime/interpreter/_agents.py +196 -0
- verdog_runtime/interpreter/_calls.py +149 -0
- verdog_runtime/interpreter/_continuation.py +611 -0
- verdog_runtime/interpreter/_errors.py +9 -0
- verdog_runtime/interpreter/_invocations.py +367 -0
- verdog_runtime/interpreter/execution.py +4221 -0
- verdog_runtime/interpreter/features.py +198 -0
- verdog_runtime/interpreter/nodes/__init__.py +9 -0
- verdog_runtime/interpreter/nodes/agent.py +149 -0
- verdog_runtime/interpreter/nodes/feature.py +24 -0
- verdog_runtime/interpreter/nodes/python.py +21 -0
- verdog_runtime/interpreter/policies.py +13 -0
- verdog_runtime/interpreter/validation.py +500 -0
- verdog_runtime/py.typed +0 -0
- verdog_runtime-0.1.0.dist-info/METADATA +73 -0
- verdog_runtime-0.1.0.dist-info/RECORD +58 -0
- verdog_runtime-0.1.0.dist-info/WHEEL +5 -0
- verdog_runtime-0.1.0.dist-info/licenses/LICENSE +235 -0
- verdog_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""The Verdog workflow runtime.
|
|
2
|
+
|
|
3
|
+
Byte-identical for every project, which is why it is one installed package rather
|
|
4
|
+
than a copy inside each: one shared runtime keeps execution types identical across
|
|
5
|
+
projects and lets subroutines interoperate directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cancellation import (
|
|
9
|
+
CancellationToken as CancellationToken,
|
|
10
|
+
ExecutionCancelled as ExecutionCancelled,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ["CancellationToken", "ExecutionCancelled"]
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""Reference immutable run artifacts without copying them at checkpoint boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, TypeAlias, cast
|
|
12
|
+
|
|
13
|
+
from ._run_metadata import FileSignature, file_signature, fsync_directory
|
|
14
|
+
from ._run_model import CONTROL_DIRECTORY, RunStoreError
|
|
15
|
+
from ._relative_path import strict_posix_relative_parts
|
|
16
|
+
|
|
17
|
+
_ARTIFACT_REFERENCES_VERSION = 1
|
|
18
|
+
_COPY_CHUNK_SIZE = 1024 * 1024
|
|
19
|
+
_FICLONE = 0x40049409
|
|
20
|
+
_INVOCATION_METADATA = ".verdog-invocation.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ArtifactDirectory:
|
|
25
|
+
relative: Path
|
|
26
|
+
mode: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ArtifactFile:
|
|
31
|
+
relative: Path
|
|
32
|
+
mode: int
|
|
33
|
+
size: int
|
|
34
|
+
sha256: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ArtifactReferences: TypeAlias = tuple[
|
|
38
|
+
tuple[ArtifactDirectory, ...], tuple[ArtifactFile, ...]
|
|
39
|
+
]
|
|
40
|
+
ArtifactCache: TypeAlias = dict[Path, tuple[FileSignature, ArtifactFile]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _artifact_relative(name: str) -> Path:
|
|
44
|
+
parts = strict_posix_relative_parts(name)
|
|
45
|
+
if parts is None or CONTROL_DIRECTORY in parts:
|
|
46
|
+
raise RunStoreError(
|
|
47
|
+
f"unsafe checkpoint artifact path: {name or '<empty>'}",
|
|
48
|
+
code="checkpoint.artifact_unsafe",
|
|
49
|
+
details={"path": name},
|
|
50
|
+
)
|
|
51
|
+
return Path(*parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _artifact_mode(value: os.stat_result) -> int:
|
|
55
|
+
return stat.S_IMODE(value.st_mode) & 0o777
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _integrity_error(relative: Path) -> None:
|
|
59
|
+
raise RunStoreError(
|
|
60
|
+
f"checkpoint artifact failed its integrity check: {relative.as_posix()}",
|
|
61
|
+
code="checkpoint.artifact_corrupt",
|
|
62
|
+
details={"path": relative.as_posix()},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _artifact_path(root: Path, relative: Path, *, directory: bool) -> Path:
|
|
67
|
+
target = root.joinpath(*relative.parts)
|
|
68
|
+
try:
|
|
69
|
+
canonical_root = root.resolve(strict=True)
|
|
70
|
+
resolved = target.resolve(strict=True)
|
|
71
|
+
mode = target.lstat().st_mode
|
|
72
|
+
except OSError as error:
|
|
73
|
+
raise RunStoreError(
|
|
74
|
+
f"checkpoint artifact is unavailable: {relative.as_posix()}",
|
|
75
|
+
code="checkpoint.artifact_unavailable",
|
|
76
|
+
details={"path": relative.as_posix()},
|
|
77
|
+
) from error
|
|
78
|
+
if (
|
|
79
|
+
root.is_symlink()
|
|
80
|
+
or target.is_symlink()
|
|
81
|
+
or not (stat.S_ISDIR(mode) if directory else stat.S_ISREG(mode))
|
|
82
|
+
or resolved != canonical_root.joinpath(*relative.parts)
|
|
83
|
+
):
|
|
84
|
+
raise RunStoreError(
|
|
85
|
+
f"checkpoint artifact is unsafe: {relative.as_posix()}",
|
|
86
|
+
code="checkpoint.artifact_unsafe",
|
|
87
|
+
details={"path": relative.as_posix()},
|
|
88
|
+
)
|
|
89
|
+
return resolved
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _artifact_file_path(root: Path, relative: Path) -> Path:
|
|
93
|
+
return _artifact_path(root, relative, directory=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _scan_artifacts(
|
|
97
|
+
root: Path,
|
|
98
|
+
) -> tuple[dict[Path, FileSignature], dict[Path, FileSignature]]:
|
|
99
|
+
_artifact_path(root, Path(), directory=True)
|
|
100
|
+
directories: dict[Path, FileSignature] = {}
|
|
101
|
+
files: dict[Path, FileSignature] = {}
|
|
102
|
+
pending = [Path()]
|
|
103
|
+
while pending:
|
|
104
|
+
base = pending.pop()
|
|
105
|
+
directory = root / base
|
|
106
|
+
try:
|
|
107
|
+
with os.scandir(directory) as scan:
|
|
108
|
+
entries = sorted(scan, key=lambda item: item.name)
|
|
109
|
+
report_directory = base == Path() or any(
|
|
110
|
+
entry.name == _INVOCATION_METADATA for entry in entries
|
|
111
|
+
)
|
|
112
|
+
for entry in entries:
|
|
113
|
+
if entry.name == CONTROL_DIRECTORY:
|
|
114
|
+
continue
|
|
115
|
+
if report_directory and entry.name in ("config.md", "stats.md"):
|
|
116
|
+
continue
|
|
117
|
+
if base == Path() and entry.name == "trace.log":
|
|
118
|
+
continue
|
|
119
|
+
relative = _artifact_relative((base / entry.name).as_posix())
|
|
120
|
+
metadata = entry.stat(follow_symlinks=False)
|
|
121
|
+
signature = file_signature(metadata)
|
|
122
|
+
if stat.S_ISDIR(metadata.st_mode):
|
|
123
|
+
directories[relative] = signature
|
|
124
|
+
pending.append(relative)
|
|
125
|
+
elif stat.S_ISREG(metadata.st_mode):
|
|
126
|
+
files[relative] = signature
|
|
127
|
+
else:
|
|
128
|
+
raise RunStoreError(
|
|
129
|
+
f"artifact is not a regular file or directory: {root / relative}",
|
|
130
|
+
code="checkpoint.artifact_unsafe",
|
|
131
|
+
details={"path": str(root / relative)},
|
|
132
|
+
)
|
|
133
|
+
except OSError as error:
|
|
134
|
+
raise RunStoreError(
|
|
135
|
+
f"artifact directory changed while checkpointing: {directory}",
|
|
136
|
+
code="checkpoint.artifact_unstable",
|
|
137
|
+
details={"path": str(directory)},
|
|
138
|
+
) from error
|
|
139
|
+
return directories, files
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read_artifact(
|
|
143
|
+
root: Path, relative: Path, signature: FileSignature, *, flush: bool
|
|
144
|
+
) -> ArtifactFile:
|
|
145
|
+
path = _artifact_file_path(root, relative)
|
|
146
|
+
try:
|
|
147
|
+
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
148
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
149
|
+
if file_signature(os.fstat(stream.fileno())) != signature:
|
|
150
|
+
_integrity_error(relative)
|
|
151
|
+
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
152
|
+
if flush:
|
|
153
|
+
os.fsync(stream.fileno())
|
|
154
|
+
if (
|
|
155
|
+
file_signature(os.fstat(stream.fileno())) != signature
|
|
156
|
+
or file_signature(path.lstat()) != signature
|
|
157
|
+
):
|
|
158
|
+
_integrity_error(relative)
|
|
159
|
+
except OSError as error:
|
|
160
|
+
raise RunStoreError(
|
|
161
|
+
f"checkpoint artifact is unreadable: {relative.as_posix()}",
|
|
162
|
+
code="checkpoint.artifact_unavailable",
|
|
163
|
+
details={"path": relative.as_posix()},
|
|
164
|
+
) from error
|
|
165
|
+
return ArtifactFile(
|
|
166
|
+
relative, stat.S_IMODE(signature[2]) & 0o777, signature[3], digest
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _write_all(descriptor: int, value: bytes) -> None:
|
|
171
|
+
offset = 0
|
|
172
|
+
while offset < len(value):
|
|
173
|
+
written = os.write(descriptor, value[offset:])
|
|
174
|
+
if written <= 0:
|
|
175
|
+
raise OSError("artifact write made no progress")
|
|
176
|
+
offset += written
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _transfer_and_hash(source: int, target: int | None) -> tuple[int, str]:
|
|
180
|
+
digest = hashlib.sha256()
|
|
181
|
+
size = 0
|
|
182
|
+
os.lseek(source, 0, os.SEEK_SET)
|
|
183
|
+
while chunk := os.read(source, _COPY_CHUNK_SIZE):
|
|
184
|
+
digest.update(chunk)
|
|
185
|
+
size += len(chunk)
|
|
186
|
+
if target is not None:
|
|
187
|
+
_write_all(target, chunk)
|
|
188
|
+
return size, digest.hexdigest()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _reference_mode(value: Mapping[str, Any], path: Path) -> int:
|
|
192
|
+
mode = value.get("mode")
|
|
193
|
+
if not isinstance(mode, int) or isinstance(mode, bool) or mode < 0 or mode > 0o777:
|
|
194
|
+
raise RunStoreError(
|
|
195
|
+
f"checkpoint artifact has an invalid mode: {path}",
|
|
196
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
197
|
+
details={"path": str(path), "field": "mode"},
|
|
198
|
+
)
|
|
199
|
+
return mode
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _reference_string(value: Mapping[str, Any], key: str, path: Path) -> str:
|
|
203
|
+
found = value.get(key)
|
|
204
|
+
if not isinstance(found, str) or not found:
|
|
205
|
+
raise RunStoreError(
|
|
206
|
+
f"checkpoint artifact has an invalid {key}: {path}",
|
|
207
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
208
|
+
details={"path": str(path), "field": key},
|
|
209
|
+
)
|
|
210
|
+
return found
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _reference_size(value: Mapping[str, Any], path: Path) -> int:
|
|
214
|
+
found = value.get("size")
|
|
215
|
+
if not isinstance(found, int) or isinstance(found, bool) or found < 0:
|
|
216
|
+
raise RunStoreError(
|
|
217
|
+
f"checkpoint artifact has an invalid size: {path}",
|
|
218
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
219
|
+
details={"path": str(path), "field": "size"},
|
|
220
|
+
)
|
|
221
|
+
return found
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _reference_records(value: Mapping[str, Any], key: str, path: Path) -> list[object]:
|
|
225
|
+
records = value.get(key)
|
|
226
|
+
if not isinstance(records, list):
|
|
227
|
+
raise RunStoreError(
|
|
228
|
+
f"checkpoint artifact manifest has no valid {key}: {path}",
|
|
229
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
230
|
+
details={"path": str(path), "field": key},
|
|
231
|
+
)
|
|
232
|
+
return cast(list[object], records)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _reference_directories(
|
|
236
|
+
value: Mapping[str, Any], path: Path
|
|
237
|
+
) -> tuple[ArtifactDirectory, ...]:
|
|
238
|
+
found: list[ArtifactDirectory] = []
|
|
239
|
+
for item in _reference_records(value, "directories", path):
|
|
240
|
+
if not isinstance(item, dict):
|
|
241
|
+
raise RunStoreError(
|
|
242
|
+
f"checkpoint artifact directory is invalid: {path}",
|
|
243
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
244
|
+
details={"path": str(path)},
|
|
245
|
+
)
|
|
246
|
+
record = cast(dict[str, Any], item)
|
|
247
|
+
found.append(
|
|
248
|
+
ArtifactDirectory(
|
|
249
|
+
_artifact_relative(_reference_string(record, "path", path)),
|
|
250
|
+
_reference_mode(record, path),
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
return tuple(sorted(found, key=lambda item: item.relative.as_posix()))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _reference_files(value: Mapping[str, Any], path: Path) -> tuple[ArtifactFile, ...]:
|
|
257
|
+
found: list[ArtifactFile] = []
|
|
258
|
+
for item in _reference_records(value, "files", path):
|
|
259
|
+
if not isinstance(item, dict):
|
|
260
|
+
raise RunStoreError(
|
|
261
|
+
f"checkpoint artifact file is invalid: {path}",
|
|
262
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
263
|
+
details={"path": str(path)},
|
|
264
|
+
)
|
|
265
|
+
record = cast(dict[str, Any], item)
|
|
266
|
+
digest = _reference_string(record, "sha256", path)
|
|
267
|
+
if len(digest) != 64 or any(
|
|
268
|
+
character not in "0123456789abcdef" for character in digest
|
|
269
|
+
):
|
|
270
|
+
raise RunStoreError(
|
|
271
|
+
f"checkpoint artifact digest is invalid: {path}",
|
|
272
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
273
|
+
details={"path": str(path), "field": "sha256"},
|
|
274
|
+
)
|
|
275
|
+
found.append(
|
|
276
|
+
ArtifactFile(
|
|
277
|
+
_artifact_relative(_reference_string(record, "path", path)),
|
|
278
|
+
_reference_mode(record, path),
|
|
279
|
+
_reference_size(record, path),
|
|
280
|
+
digest,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
return tuple(sorted(found, key=lambda item: item.relative.as_posix()))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _validate_reference_layout(
|
|
287
|
+
directories: Sequence[ArtifactDirectory],
|
|
288
|
+
files: Sequence[ArtifactFile],
|
|
289
|
+
manifest_path: Path,
|
|
290
|
+
) -> None:
|
|
291
|
+
directory_paths = {item.relative for item in directories}
|
|
292
|
+
file_paths = {item.relative for item in files}
|
|
293
|
+
if (
|
|
294
|
+
len(directory_paths) != len(directories)
|
|
295
|
+
or len(file_paths) != len(files)
|
|
296
|
+
or directory_paths & file_paths
|
|
297
|
+
):
|
|
298
|
+
raise RunStoreError(
|
|
299
|
+
f"checkpoint artifact paths are ambiguous: {manifest_path}",
|
|
300
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
301
|
+
details={"path": str(manifest_path)},
|
|
302
|
+
)
|
|
303
|
+
missing_parent = next(
|
|
304
|
+
(
|
|
305
|
+
relative
|
|
306
|
+
for relative in (*directory_paths, *file_paths)
|
|
307
|
+
if relative.parent != Path() and relative.parent not in directory_paths
|
|
308
|
+
),
|
|
309
|
+
None,
|
|
310
|
+
)
|
|
311
|
+
if missing_parent is not None:
|
|
312
|
+
raise RunStoreError(
|
|
313
|
+
f"checkpoint artifact parent is missing: {missing_parent.as_posix()}",
|
|
314
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
315
|
+
details={"path": missing_parent.as_posix()},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def decode_artifact_references(
|
|
320
|
+
raw: object, manifest_path: Path, /
|
|
321
|
+
) -> ArtifactReferences | None:
|
|
322
|
+
"""Decode artifact metadata already read from one checkpoint manifest."""
|
|
323
|
+
|
|
324
|
+
if raw is None:
|
|
325
|
+
return None
|
|
326
|
+
if not isinstance(raw, dict):
|
|
327
|
+
raise RunStoreError(
|
|
328
|
+
f"checkpoint has no valid artifact references: {manifest_path}",
|
|
329
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
330
|
+
details={"path": str(manifest_path)},
|
|
331
|
+
)
|
|
332
|
+
artifact = cast(dict[str, Any], raw)
|
|
333
|
+
if (
|
|
334
|
+
type(artifact.get("schema_version")) is not int
|
|
335
|
+
or artifact.get("schema_version") != _ARTIFACT_REFERENCES_VERSION
|
|
336
|
+
or artifact.get("kind") != "references"
|
|
337
|
+
):
|
|
338
|
+
raise RunStoreError(
|
|
339
|
+
f"checkpoint has no valid artifact references: {manifest_path}",
|
|
340
|
+
code="checkpoint.artifact_manifest_invalid",
|
|
341
|
+
details={"path": str(manifest_path)},
|
|
342
|
+
)
|
|
343
|
+
directories = _reference_directories(artifact, manifest_path)
|
|
344
|
+
files = _reference_files(artifact, manifest_path)
|
|
345
|
+
_validate_reference_layout(directories, files, manifest_path)
|
|
346
|
+
return directories, files
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def validate_artifact_extension(
|
|
350
|
+
previous: ArtifactReferences | None, current: ArtifactReferences
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Previously published artifacts are immutable; directories may gain children."""
|
|
353
|
+
|
|
354
|
+
if previous is None:
|
|
355
|
+
return
|
|
356
|
+
for old, new in zip(previous, current):
|
|
357
|
+
indexed = {item.relative: item for item in new}
|
|
358
|
+
for item in old:
|
|
359
|
+
if indexed.get(item.relative) != item:
|
|
360
|
+
_integrity_error(item.relative)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def capture_artifacts(
|
|
364
|
+
source_root: Path,
|
|
365
|
+
*,
|
|
366
|
+
cache: ArtifactCache,
|
|
367
|
+
previous: ArtifactReferences | None = None,
|
|
368
|
+
) -> dict[str, object]:
|
|
369
|
+
"""Flush and inventory existing outputs, retaining verified hashes in ``cache``."""
|
|
370
|
+
|
|
371
|
+
before_directories, before_files = _scan_artifacts(source_root)
|
|
372
|
+
directories = tuple(
|
|
373
|
+
ArtifactDirectory(relative, stat.S_IMODE(signature[2]) & 0o777)
|
|
374
|
+
for relative, signature in sorted(before_directories.items())
|
|
375
|
+
)
|
|
376
|
+
files: list[ArtifactFile] = []
|
|
377
|
+
changed_directories: set[Path] = {Path()}
|
|
378
|
+
for relative, signature in sorted(before_files.items()):
|
|
379
|
+
cached = cache.get(relative)
|
|
380
|
+
if cached is not None and cached[0] == signature:
|
|
381
|
+
record = cached[1]
|
|
382
|
+
else:
|
|
383
|
+
record = _read_artifact(source_root, relative, signature, flush=True)
|
|
384
|
+
cache[relative] = (signature, record)
|
|
385
|
+
changed_directories.add(relative.parent)
|
|
386
|
+
files.append(record)
|
|
387
|
+
current = directories, tuple(files)
|
|
388
|
+
validate_artifact_extension(previous, current)
|
|
389
|
+
if (before_directories, before_files) != _scan_artifacts(source_root):
|
|
390
|
+
raise RunStoreError(
|
|
391
|
+
"run artifacts changed while checkpoint references were being captured",
|
|
392
|
+
code="checkpoint.artifact_unstable",
|
|
393
|
+
details={"output_dir": str(source_root)},
|
|
394
|
+
)
|
|
395
|
+
previous_directories: set[Path] = (
|
|
396
|
+
set() if previous is None else {item.relative for item in previous[0]}
|
|
397
|
+
)
|
|
398
|
+
for record in directories:
|
|
399
|
+
if record.relative not in previous_directories:
|
|
400
|
+
changed_directories.update((record.relative, record.relative.parent))
|
|
401
|
+
for relative in sorted(
|
|
402
|
+
changed_directories, key=lambda item: len(item.parts), reverse=True
|
|
403
|
+
):
|
|
404
|
+
fsync_directory(source_root / relative)
|
|
405
|
+
return {
|
|
406
|
+
"schema_version": _ARTIFACT_REFERENCES_VERSION,
|
|
407
|
+
"kind": "references",
|
|
408
|
+
"directories": [
|
|
409
|
+
{"path": item.relative.as_posix(), "mode": item.mode}
|
|
410
|
+
for item in directories
|
|
411
|
+
],
|
|
412
|
+
"files": [
|
|
413
|
+
{
|
|
414
|
+
"path": item.relative.as_posix(),
|
|
415
|
+
"mode": item.mode,
|
|
416
|
+
"size": item.size,
|
|
417
|
+
"sha256": item.sha256,
|
|
418
|
+
}
|
|
419
|
+
for item in files
|
|
420
|
+
],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _validate_directories(root: Path, directories: Sequence[ArtifactDirectory]) -> None:
|
|
425
|
+
_artifact_path(root, Path(), directory=True)
|
|
426
|
+
for record in directories:
|
|
427
|
+
path = _artifact_path(root, record.relative, directory=True)
|
|
428
|
+
if _artifact_mode(path.lstat()) != record.mode:
|
|
429
|
+
_integrity_error(record.relative)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def validate_artifacts(
|
|
433
|
+
source_root: Path, references: ArtifactReferences, *, cache: ArtifactCache
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Verify exactly the referenced artifacts; later outputs do not invalidate them."""
|
|
436
|
+
|
|
437
|
+
directories, files = references
|
|
438
|
+
_validate_directories(source_root, directories)
|
|
439
|
+
for record in files:
|
|
440
|
+
path = _artifact_file_path(source_root, record.relative)
|
|
441
|
+
signature = file_signature(path.lstat())
|
|
442
|
+
current = _read_artifact(source_root, record.relative, signature, flush=False)
|
|
443
|
+
if current != record:
|
|
444
|
+
_integrity_error(record.relative)
|
|
445
|
+
cache[record.relative] = (signature, current)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _try_reflink(source: int, target: int) -> bool:
|
|
449
|
+
if os.name != "posix":
|
|
450
|
+
return False
|
|
451
|
+
try:
|
|
452
|
+
import fcntl
|
|
453
|
+
|
|
454
|
+
fcntl.ioctl(target, _FICLONE, source)
|
|
455
|
+
except OSError:
|
|
456
|
+
os.ftruncate(target, 0)
|
|
457
|
+
return False
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _materialize_artifact_bytes(
|
|
462
|
+
source: Path,
|
|
463
|
+
target: Path,
|
|
464
|
+
record: ArtifactFile,
|
|
465
|
+
expected_signature: FileSignature,
|
|
466
|
+
) -> None:
|
|
467
|
+
source_descriptor = os.open(source, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
468
|
+
try:
|
|
469
|
+
if file_signature(os.fstat(source_descriptor)) != expected_signature:
|
|
470
|
+
raise RunStoreError(
|
|
471
|
+
"checkpoint artifact changed during materialization: "
|
|
472
|
+
f"{record.relative.as_posix()}",
|
|
473
|
+
code="checkpoint.artifact_corrupt",
|
|
474
|
+
details={"path": record.relative.as_posix()},
|
|
475
|
+
)
|
|
476
|
+
target_descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
477
|
+
try:
|
|
478
|
+
cloned = _try_reflink(source_descriptor, target_descriptor)
|
|
479
|
+
size, digest = _transfer_and_hash(
|
|
480
|
+
source_descriptor, None if cloned else target_descriptor
|
|
481
|
+
)
|
|
482
|
+
os.fsync(target_descriptor)
|
|
483
|
+
finally:
|
|
484
|
+
os.close(target_descriptor)
|
|
485
|
+
current_signature = file_signature(source.lstat())
|
|
486
|
+
if (
|
|
487
|
+
file_signature(os.fstat(source_descriptor)) != expected_signature
|
|
488
|
+
or current_signature != expected_signature
|
|
489
|
+
or size != record.size
|
|
490
|
+
or digest != record.sha256
|
|
491
|
+
):
|
|
492
|
+
raise RunStoreError(
|
|
493
|
+
"checkpoint artifact failed its integrity check: "
|
|
494
|
+
f"{record.relative.as_posix()}",
|
|
495
|
+
code="checkpoint.artifact_corrupt",
|
|
496
|
+
details={"path": record.relative.as_posix()},
|
|
497
|
+
)
|
|
498
|
+
finally:
|
|
499
|
+
os.close(source_descriptor)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _copy_referenced_artifact(
|
|
503
|
+
source_root: Path, record: ArtifactFile, target: Path
|
|
504
|
+
) -> None:
|
|
505
|
+
source = _artifact_file_path(source_root, record.relative)
|
|
506
|
+
metadata = source.lstat()
|
|
507
|
+
if _artifact_mode(metadata) != record.mode:
|
|
508
|
+
_integrity_error(record.relative)
|
|
509
|
+
expected_signature = file_signature(metadata)
|
|
510
|
+
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
511
|
+
try:
|
|
512
|
+
_materialize_artifact_bytes(source, target, record, expected_signature)
|
|
513
|
+
except RunStoreError:
|
|
514
|
+
target.unlink(missing_ok=True)
|
|
515
|
+
raise
|
|
516
|
+
except OSError as error:
|
|
517
|
+
target.unlink(missing_ok=True)
|
|
518
|
+
raise RunStoreError(
|
|
519
|
+
"checkpoint artifact could not be materialized: "
|
|
520
|
+
f"{record.relative.as_posix()}",
|
|
521
|
+
code="checkpoint.artifact_unavailable",
|
|
522
|
+
details={"path": record.relative.as_posix()},
|
|
523
|
+
) from error
|
|
524
|
+
target.chmod(record.mode)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def materialize_artifact_references(
|
|
528
|
+
source_root: Path,
|
|
529
|
+
destination: Path,
|
|
530
|
+
directories: Sequence[ArtifactDirectory],
|
|
531
|
+
files: Sequence[ArtifactFile],
|
|
532
|
+
) -> None:
|
|
533
|
+
_validate_directories(source_root, directories)
|
|
534
|
+
for record in sorted(directories, key=lambda item: len(item.relative.parts)):
|
|
535
|
+
destination.joinpath(*record.relative.parts).mkdir(mode=0o700)
|
|
536
|
+
for record in files:
|
|
537
|
+
_copy_referenced_artifact(
|
|
538
|
+
source_root,
|
|
539
|
+
record,
|
|
540
|
+
destination.joinpath(*record.relative.parts),
|
|
541
|
+
)
|
|
542
|
+
for record in sorted(
|
|
543
|
+
directories, key=lambda item: len(item.relative.parts), reverse=True
|
|
544
|
+
):
|
|
545
|
+
directory = destination.joinpath(*record.relative.parts)
|
|
546
|
+
fsync_directory(directory)
|
|
547
|
+
directory.chmod(record.mode)
|
|
548
|
+
fsync_directory(destination)
|