Files
boshu2__agentops/scripts/publish-generated-projections.py

1455 lines
50 KiB
Python
Executable File

#!/usr/bin/env python3
"""Render and publish every owner-mapped generated projection transactionally.
The publisher never uses Git for classification or recovery. It renders in a
disposable repository copy, snapshots exact live filesystem state, writes each
owned target from staging, and commits the publication manifest last.
"""
from __future__ import annotations
import argparse
from contextlib import contextmanager
from datetime import datetime, timezone
import fcntl
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import signal
import stat
import subprocess
import sys
import tempfile
import time
from typing import Any, Iterator
import uuid
OWNER_MAP_SCHEMA = "generated-projection-owner-map.v1"
PUBLICATION_SCHEMA = "generated-projection-publication.v1"
RECEIPT_SCHEMA = "publication-receipt.v1"
CLASSIFICATIONS = {
"CLEAN_CURRENT",
"DIRTY_PRESERVE",
"MISSING",
"UNOWNED",
}
HEX64 = frozenset("0123456789abcdef")
class PublicationError(RuntimeError):
"""Publication cannot continue without violating the contract."""
class PublicationInterrupted(PublicationError):
"""A handled process signal interrupted publication."""
def canonical_bytes(value: Any) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
def digest_value(value: Any) -> str:
return hashlib.sha256(canonical_bytes(value)).hexdigest()
def sha256(payload: bytes) -> str:
return hashlib.sha256(payload).hexdigest()
def valid_digest(value: Any) -> bool:
return (
isinstance(value, str)
and len(value) == 64
and all(character in HEX64 for character in value)
)
def finalize_artifact(value: dict[str, Any]) -> dict[str, Any]:
artifact = dict(value)
artifact.pop("artifact_digest", None)
artifact["artifact_digest"] = digest_value(artifact)
return artifact
def verify_artifact(value: dict[str, Any], label: str) -> None:
claimed = value.get("artifact_digest")
identity = {key: item for key, item in value.items() if key != "artifact_digest"}
if not valid_digest(claimed) or digest_value(identity) != claimed:
raise PublicationError(f"{label} artifact_digest is invalid")
def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise PublicationError(f"duplicate JSON key: {key}")
result[key] = value
return result
def load_json(path: Path, label: str | None = None) -> dict[str, Any]:
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
raise PublicationError(f"cannot read {label or path}: {exc}") from exc
decoder = json.JSONDecoder(object_pairs_hook=reject_duplicate_keys)
try:
value, end = decoder.raw_decode(text)
except json.JSONDecodeError as exc:
raise PublicationError(f"{label or path} is not valid JSON: {exc}") from exc
if text[end:].strip():
raise PublicationError(f"{label or path} contains trailing JSON data")
if not isinstance(value, dict):
raise PublicationError(f"{label or path} must be a JSON object")
return value
def exact_keys(
value: dict[str, Any],
required: set[str],
label: str,
) -> None:
missing = sorted(required - value.keys())
extra = sorted(value.keys() - required)
if missing:
raise PublicationError(f"{label} missing fields: {', '.join(missing)}")
if extra:
raise PublicationError(f"{label} has unknown fields: {', '.join(extra)}")
def normalize_ref(raw: Any) -> str:
if not isinstance(raw, str) or not raw:
raise PublicationError("repository reference must be a nonempty string")
if raw == "." or "\\" in raw or raw.startswith("/") or raw.endswith("/"):
raise PublicationError(f"unsafe repository reference: {raw!r}")
if len(raw) >= 2 and raw[0].isalpha() and raw[1] == ":":
raise PublicationError(f"drive-qualified repository reference: {raw!r}")
if any(ord(character) < 32 or ord(character) == 127 for character in raw):
raise PublicationError(f"control character in repository reference: {raw!r}")
parts = raw.split("/")
if any(part in {"", ".", ".."} for part in parts):
raise PublicationError(f"non-canonical repository reference: {raw!r}")
if PurePosixPath(raw).as_posix() != raw:
raise PublicationError(f"non-POSIX repository reference: {raw!r}")
return raw
def inside(path: str, root: str) -> bool:
return path == root or path.startswith(root + "/")
def resolve_ref(
repository: Path,
reference: str,
*,
allow_leaf_symlink: bool = True,
) -> Path:
reference = normalize_ref(reference)
repository = repository.resolve()
current = repository
parts = reference.split("/")
for index, part in enumerate(parts):
current = current / part
if current.is_symlink() and (index < len(parts) - 1 or not allow_leaf_symlink):
raise PublicationError(
f"repository reference traverses a symlink: {reference}"
)
return repository / reference
def validate_owner_map(value: dict[str, Any], repository: Path) -> None:
exact_keys(
value,
{"schema_version", "publication", "owners", "artifact_digest"},
OWNER_MAP_SCHEMA,
)
if value["schema_version"] != OWNER_MAP_SCHEMA:
raise PublicationError("owner map schema_version is invalid")
verify_artifact(value, OWNER_MAP_SCHEMA)
publication = value["publication"]
if not isinstance(publication, dict):
raise PublicationError("owner map publication must be an object")
exact_keys(
publication,
{"lock_ref", "manifest_ref", "receipt_dir_ref", "transaction_dir_ref"},
"owner map publication",
)
publication_refs = [
normalize_ref(publication[field])
for field in (
"lock_ref",
"manifest_ref",
"receipt_dir_ref",
"transaction_dir_ref",
)
]
if len(set(publication_refs)) != len(publication_refs):
raise PublicationError("owner map publication references must be unique")
owners = value["owners"]
if not isinstance(owners, list) or not owners:
raise PublicationError("owner map owners must be a nonempty array")
owner_ids: set[str] = set()
target_paths: list[str] = []
for owner_index, owner in enumerate(owners):
label = f"owners[{owner_index}]"
if not isinstance(owner, dict):
raise PublicationError(f"{label} must be an object")
exact_keys(
owner,
{"id", "generator_refs", "source_refs", "commands", "targets"},
label,
)
owner_id = owner["id"]
if (
not isinstance(owner_id, str)
or not owner_id
or owner_id in owner_ids
or not owner_id[0].isalpha()
or any(
not (character.islower() or character.isdigit() or character == "-")
for character in owner_id
)
):
raise PublicationError(f"{label}.id is invalid or duplicated")
owner_ids.add(owner_id)
for field in ("generator_refs", "source_refs"):
references = owner[field]
if (
not isinstance(references, list)
or not references
or len(references) != len(set(references))
):
raise PublicationError(f"{label}.{field} must be nonempty and unique")
for reference in references:
path = resolve_ref(
repository,
normalize_ref(reference),
allow_leaf_symlink=False,
)
if not os.path.lexists(path):
raise PublicationError(
f"{label}.{field} reference is missing: {reference}"
)
commands = owner["commands"]
if not isinstance(commands, list) or not commands:
raise PublicationError(f"{label}.commands must be nonempty")
for command_index, command in enumerate(commands):
if (
not isinstance(command, list)
or not command
or any(not isinstance(arg, str) or not arg for arg in command)
):
raise PublicationError(
f"{label}.commands[{command_index}] must be a nonempty argv"
)
targets = owner["targets"]
if not isinstance(targets, list) or not targets:
raise PublicationError(f"{label}.targets must be nonempty")
for target_index, target in enumerate(targets):
target_label = f"{label}.targets[{target_index}]"
if not isinstance(target, dict):
raise PublicationError(f"{target_label} must be an object")
exact_keys(target, {"path", "kind", "allow_missing"}, target_label)
target_path = normalize_ref(target["path"])
if target["kind"] not in {"file", "tree", "symlink", "any"}:
raise PublicationError(f"{target_label}.kind is invalid")
if not isinstance(target["allow_missing"], bool):
raise PublicationError(f"{target_label}.allow_missing must be boolean")
target_paths.append(target_path)
if len(target_paths) != len(set(target_paths)):
raise PublicationError("projection targets must have exactly one owner")
for index, left in enumerate(target_paths):
for right in target_paths[index + 1 :]:
if inside(left, right) or inside(right, left):
raise PublicationError(
f"projection targets overlap: {left} and {right}"
)
for reference in publication_refs:
if any(
inside(reference, target) or inside(target, reference)
for target in target_paths
):
raise PublicationError(
f"publication state overlaps generated target: {reference}"
)
def all_targets(owner_map: dict[str, Any]) -> list[dict[str, Any]]:
targets = []
for owner in owner_map["owners"]:
for target in owner["targets"]:
targets.append({"owner": owner["id"], **target})
return sorted(targets, key=lambda item: item["path"])
def state_identity(state: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in state.items() if key != "state_digest"}
def finalize_state(state: dict[str, Any]) -> dict[str, Any]:
result = dict(state)
result["state_digest"] = digest_value(result)
return result
def validate_state(state: Any, label: str) -> None:
if not isinstance(state, dict):
raise PublicationError(f"{label} must be an object")
kind = state.get("kind")
fields = {
"missing": {"kind", "state_digest"},
"file": {"kind", "mode", "content_digest", "state_digest"},
"symlink": {
"kind",
"mode",
"link_target",
"content_digest",
"state_digest",
},
"directory": {"kind", "mode", "entries", "state_digest"},
}
if kind not in fields:
raise PublicationError(f"{label}.kind is invalid")
exact_keys(state, fields[kind], label)
if not valid_digest(state["state_digest"]):
raise PublicationError(f"{label}.state_digest is invalid")
if digest_value(state_identity(state)) != state["state_digest"]:
raise PublicationError(f"{label}.state_digest does not bind state")
if kind == "missing":
return
mode = state["mode"]
if (
not isinstance(mode, str)
or len(mode) != 4
or any(character not in "01234567" for character in mode)
):
raise PublicationError(f"{label}.mode is invalid")
if kind in {"file", "symlink"} and not valid_digest(state["content_digest"]):
raise PublicationError(f"{label}.content_digest is invalid")
if kind == "symlink" and not isinstance(state["link_target"], str):
raise PublicationError(f"{label}.link_target is invalid")
if kind != "directory":
return
entries = state["entries"]
if not isinstance(entries, list):
raise PublicationError(f"{label}.entries must be an array")
paths: list[str] = []
for index, entry in enumerate(entries):
entry_label = f"{label}.entries[{index}]"
if not isinstance(entry, dict):
raise PublicationError(f"{entry_label} must be an object")
entry_kind = entry.get("kind")
entry_fields = {
"directory": {"path", "kind", "mode"},
"file": {"path", "kind", "mode", "content_digest"},
"symlink": {
"path",
"kind",
"mode",
"link_target",
"content_digest",
},
}
if entry_kind not in entry_fields:
raise PublicationError(f"{entry_label}.kind is invalid")
exact_keys(entry, entry_fields[entry_kind], entry_label)
paths.append(normalize_ref(entry["path"]))
if (
not isinstance(entry["mode"], str)
or len(entry["mode"]) != 4
or any(character not in "01234567" for character in entry["mode"])
):
raise PublicationError(f"{entry_label}.mode is invalid")
if entry_kind in {"file", "symlink"} and not valid_digest(
entry["content_digest"]
):
raise PublicationError(f"{entry_label}.content_digest is invalid")
if entry_kind == "symlink" and not isinstance(entry["link_target"], str):
raise PublicationError(f"{entry_label}.link_target is invalid")
if paths != sorted(set(paths)):
raise PublicationError(f"{label}.entries must be sorted and unique")
def mode_of(path: Path) -> str:
return f"{stat.S_IMODE(path.lstat().st_mode):04o}"
def leaf_state(path: Path, relative: str) -> dict[str, Any]:
metadata = path.lstat()
mode = f"{stat.S_IMODE(metadata.st_mode):04o}"
if path.is_symlink():
target = os.readlink(path)
return {
"path": relative,
"kind": "symlink",
"mode": mode,
"link_target": target,
"content_digest": sha256(target.encode("utf-8")),
}
if path.is_file():
return {
"path": relative,
"kind": "file",
"mode": mode,
"content_digest": sha256(path.read_bytes()),
}
if path.is_dir():
return {
"path": relative,
"kind": "directory",
"mode": mode,
}
raise PublicationError(f"unsupported filesystem kind: {path}")
def snapshot_path(path: Path) -> dict[str, Any]:
if not os.path.lexists(path):
return finalize_state({"kind": "missing"})
if path.is_symlink():
target = os.readlink(path)
return finalize_state(
{
"kind": "symlink",
"mode": mode_of(path),
"link_target": target,
"content_digest": sha256(target.encode("utf-8")),
}
)
if path.is_file():
return finalize_state(
{
"kind": "file",
"mode": mode_of(path),
"content_digest": sha256(path.read_bytes()),
}
)
if not path.is_dir():
raise PublicationError(f"unsupported filesystem kind: {path}")
entries: list[dict[str, Any]] = []
for child in sorted(path.rglob("*")):
relative = child.relative_to(path).as_posix()
entries.append(leaf_state(child, relative))
if child.is_symlink():
continue
return finalize_state(
{
"kind": "directory",
"mode": mode_of(path),
"entries": entries,
}
)
def aggregate_states(
rows: list[dict[str, Any]],
field: str,
) -> str:
return digest_value(
[
{
"owner": row["owner"],
"path": row["path"],
"state": row[field],
}
for row in sorted(rows, key=lambda item: item["path"])
]
)
def expected_kind_matches(target: dict[str, Any], state: dict[str, Any]) -> bool:
if state["kind"] == "missing":
return bool(target["allow_missing"])
expected = target["kind"]
return (
expected == "any"
or (expected == "file" and state["kind"] == "file")
or (expected == "tree" and state["kind"] == "directory")
or (expected == "symlink" and state["kind"] == "symlink")
)
def prior_owned_entries(
prior_manifest: dict[str, Any] | None,
target: dict[str, Any],
) -> set[str]:
if prior_manifest is None:
return set()
for row in prior_manifest.get("targets", []):
if row.get("owner") == target["owner"] and row.get("path") == target["path"]:
state = row.get("state") or {}
if state.get("kind") == "directory":
return {
entry["path"]
for entry in state.get("entries", [])
if isinstance(entry, dict) and isinstance(entry.get("path"), str)
}
return {"."}
return set()
def classify_target(
target: dict[str, Any],
before: dict[str, Any],
rendered: dict[str, Any],
prior_manifest: dict[str, Any] | None,
) -> str:
if before["kind"] == "missing":
return "MISSING"
if before["state_digest"] == rendered["state_digest"]:
return "CLEAN_CURRENT"
prior_entries = prior_owned_entries(prior_manifest, target)
if before["kind"] != rendered["kind"] and not prior_entries:
return "UNOWNED"
if before["kind"] == "directory":
before_entries = {entry["path"] for entry in before["entries"]}
rendered_entries = (
{entry["path"] for entry in rendered["entries"]}
if rendered["kind"] == "directory"
else set()
)
unowned = (before_entries - rendered_entries) - prior_entries
if unowned:
return "UNOWNED"
return "DIRTY_PRESERVE"
def scan_tree(
root: Path,
*,
excluded_roots: list[str] | None = None,
) -> dict[str, dict[str, Any]]:
excluded_roots = excluded_roots or []
result: dict[str, dict[str, Any]] = {}
for path in sorted(root.rglob("*")):
relative = path.relative_to(root).as_posix()
if (
inside(relative, ".git")
or inside(relative, ".agents")
or "__pycache__" in path.parts
or path.suffix in {".pyc", ".pyo"}
or any(inside(relative, excluded) for excluded in excluded_roots)
):
continue
result[relative] = leaf_state(path, relative)
return result
def changed_paths(
before: dict[str, dict[str, Any]],
after: dict[str, dict[str, Any]],
) -> list[str]:
return sorted(
path for path in set(before) | set(after) if before.get(path) != after.get(path)
)
def stage_ignore(directory: str, names: list[str]) -> set[str]:
ignored = {"__pycache__"}
if Path(directory).name == "":
ignored.update({".git", ".agents"})
ignored.update(name for name in names if name.endswith((".pyc", ".pyo")))
if ".git" in names:
ignored.add(".git")
if ".agents" in names:
ignored.add(".agents")
return ignored
def run_owner(
owner: dict[str, Any],
stage: Path,
environment: dict[str, str],
) -> None:
allowed = [target["path"] for target in owner["targets"]]
before = scan_tree(stage)
for command in owner["commands"]:
result = subprocess.run(
command,
cwd=stage,
env=environment,
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise PublicationError(
f"owner {owner['id']} render failed ({result.returncode}): {detail}"
)
after = scan_tree(stage)
outside = [
path
for path in changed_paths(before, after)
if not any(inside(path, target) for target in allowed)
]
if outside:
raise PublicationError(
f"owner {owner['id']} mutated undeclared staged paths: "
+ ", ".join(outside)
)
def render_complete(
repository: Path,
owner_map: dict[str, Any],
) -> tuple[tempfile.TemporaryDirectory[str], Path, list[dict[str, Any]]]:
temporary = tempfile.TemporaryDirectory(prefix="ao-projection-stage-")
stage = Path(temporary.name) / "repository"
shutil.copytree(
repository,
stage,
symlinks=True,
ignore=stage_ignore,
)
environment = dict(os.environ)
environment.update(
{
"AGENTOPS_PUBLICATION_STAGE_ROOT": str(stage),
"AGENTOPS_PUBLICATION": "1",
"PYTHONDONTWRITEBYTECODE": "1",
}
)
for owner in owner_map["owners"]:
run_owner(owner, stage, environment)
targets = []
for target in all_targets(owner_map):
state = snapshot_path(resolve_ref(stage, target["path"]))
if not expected_kind_matches(target, state):
raise PublicationError(
f"owner {target['owner']} rendered unexpected kind for "
f"{target['path']}: {state['kind']}"
)
targets.append({**target, "rendered": state})
first_digest = aggregate_states(targets, "rendered")
for owner in owner_map["owners"]:
run_owner(owner, stage, environment)
second = []
for target in all_targets(owner_map):
state = snapshot_path(resolve_ref(stage, target["path"]))
second.append({**target, "rendered": state})
second_digest = aggregate_states(second, "rendered")
if first_digest != second_digest:
raise PublicationError("generated projections are not byte-and-mode idempotent")
return temporary, stage, targets
def atomic_write_bytes(path: Path, payload: bytes, mode: int = 0o644) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.",
suffix=".tmp",
dir=path.parent,
)
try:
os.fchmod(descriptor, mode)
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
fsync_directory(path.parent)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def atomic_write_json(path: Path, value: dict[str, Any]) -> None:
atomic_write_bytes(path, canonical_bytes(value) + b"\n")
def fsync_directory(path: Path) -> None:
descriptor = os.open(path, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def fsync_tree(path: Path) -> None:
if path.is_symlink():
fsync_directory(path.parent)
return
if path.is_file():
descriptor = os.open(path, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
fsync_directory(path.parent)
return
if path.is_dir():
for child in sorted(path.rglob("*"), reverse=True):
if child.is_symlink():
continue
if child.is_file():
descriptor = os.open(child, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
elif child.is_dir():
fsync_directory(child)
fsync_directory(path)
def remove_path(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink()
elif path.is_dir():
shutil.rmtree(path)
def copy_path_exact(source: Path, destination: Path) -> None:
if os.path.lexists(destination):
remove_path(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
if source.is_symlink():
os.symlink(os.readlink(source), destination)
elif source.is_file():
shutil.copy2(source, destination, follow_symlinks=False)
elif source.is_dir():
shutil.copytree(
source,
destination,
symlinks=True,
copy_function=shutil.copy2,
)
shutil.copystat(source, destination, follow_symlinks=False)
else:
raise PublicationError(f"cannot copy unsupported path: {source}")
def build_recovery_bundle(
repository: Path,
transaction_root: Path,
transaction_id: str,
rows: list[dict[str, Any]],
manifest_ref: str,
owner_map_digest: str,
) -> tuple[Path, dict[str, Any]]:
bundle = transaction_root / transaction_id
bundle.mkdir(parents=True, exist_ok=False)
recovery_rows = []
for index, row in enumerate(rows):
source = resolve_ref(repository, row["path"])
slot_ref = f"targets/{index}"
slot = bundle / slot_ref
if row["before"]["kind"] != "missing":
copy_path_exact(source, slot)
fsync_tree(slot)
if snapshot_path(slot)["state_digest"] != row["before"]["state_digest"]:
raise PublicationError(f"recovery snapshot mismatch for {row['path']}")
recovery_rows.append(
{
"path": row["path"],
"slot_ref": slot_ref,
"before": row["before"],
}
)
journal = finalize_artifact(
{
"schema_version": "generated-projection-recovery.v1",
"transaction_id": transaction_id,
"owner_map_digest": owner_map_digest,
"manifest_ref": manifest_ref,
"targets": recovery_rows,
}
)
atomic_write_json(bundle / "recovery.json", journal)
fsync_directory(bundle)
fsync_directory(transaction_root)
return bundle, journal
def restore_bundle(
repository: Path,
bundle: Path,
journal: dict[str, Any],
) -> None:
for row in journal["targets"]:
target = resolve_ref(repository, row["path"])
if os.path.lexists(target):
remove_path(target)
if row["before"]["kind"] != "missing":
slot = bundle / normalize_ref(row["slot_ref"])
copy_path_exact(slot, target)
fsync_tree(target)
fsync_directory(target.parent)
if snapshot_path(target)["state_digest"] != row["before"]["state_digest"]:
raise PublicationError(f"recovery did not restore {row['path']}")
transaction_id = journal["transaction_id"]
parents = {
resolve_ref(repository, row["path"]).parent for row in journal["targets"]
}
for parent in parents:
for prefix in (
f".ao-publish-old-{transaction_id}-",
f".ao-publish-new-{transaction_id}-",
):
for leftover in parent.glob(prefix + "*"):
remove_path(leftover)
fsync_directory(parent)
def load_publication_manifest(path: Path) -> dict[str, Any] | None:
if not path.is_file():
return None
value = load_json(path, "publication manifest")
exact_keys(
value,
{
"schema_version",
"transaction_id",
"owner_map_digest",
"input_digest",
"rendered_digest",
"targets",
"published_at",
"artifact_digest",
},
PUBLICATION_SCHEMA,
)
if value["schema_version"] != PUBLICATION_SCHEMA:
raise PublicationError("publication manifest schema_version is invalid")
verify_artifact(value, PUBLICATION_SCHEMA)
for field in ("owner_map_digest", "input_digest", "rendered_digest"):
if not valid_digest(value[field]):
raise PublicationError(f"publication manifest {field} is invalid")
if (
not isinstance(value["transaction_id"], str)
or not value["transaction_id"]
or not isinstance(value["published_at"], str)
):
raise PublicationError("publication manifest identity is invalid")
targets = value["targets"]
if not isinstance(targets, list) or not targets:
raise PublicationError("publication manifest targets must be nonempty")
paths: list[str] = []
for index, row in enumerate(targets):
label = f"publication manifest targets[{index}]"
if not isinstance(row, dict):
raise PublicationError(f"{label} must be an object")
exact_keys(row, {"owner", "path", "declared_kind", "state"}, label)
if not isinstance(row["owner"], str) or not row["owner"]:
raise PublicationError(f"{label}.owner is invalid")
paths.append(normalize_ref(row["path"]))
if row["declared_kind"] not in {"file", "tree", "symlink", "any"}:
raise PublicationError(f"{label}.declared_kind is invalid")
validate_state(row["state"], f"{label}.state")
if paths != sorted(set(paths)):
raise PublicationError(
"publication manifest target paths must be sorted and unique"
)
return value
def cleanup_bundle(bundle: Path) -> None:
shutil.rmtree(bundle)
fsync_directory(bundle.parent)
def recover_pending(
repository: Path,
transaction_root: Path,
owner_map: dict[str, Any],
*,
allow_mutation: bool,
) -> tuple[int, int]:
if not transaction_root.exists():
return 0, 0
bundles = sorted(path for path in transaction_root.iterdir() if path.is_dir())
if bundles and not allow_mutation:
raise PublicationError(
"pending publication recovery exists; check mode is read-only, "
"run --recover-only or --write"
)
restored = 0
for bundle in bundles:
journal = load_json(bundle / "recovery.json", "publication recovery journal")
exact_keys(
journal,
{
"schema_version",
"transaction_id",
"owner_map_digest",
"manifest_ref",
"targets",
"artifact_digest",
},
"publication recovery journal",
)
if journal["schema_version"] != "generated-projection-recovery.v1":
raise PublicationError("recovery journal schema_version is invalid")
verify_artifact(journal, "publication recovery journal")
expected_paths = [target["path"] for target in all_targets(owner_map)]
journal_paths = [row.get("path") for row in journal["targets"]]
if (
journal["owner_map_digest"] != owner_map["artifact_digest"]
or journal_paths != expected_paths
):
raise PublicationError(
"pending recovery does not bind the current owner map and targets"
)
manifest_path = resolve_ref(
repository,
journal["manifest_ref"],
allow_leaf_symlink=False,
)
manifest = load_publication_manifest(manifest_path)
committed = (
manifest is not None
and manifest["transaction_id"] == journal["transaction_id"]
)
if committed:
manifest_states = {
row["path"]: row["state"]["state_digest"] for row in manifest["targets"]
}
committed = all(
snapshot_path(resolve_ref(repository, row["path"]))["state_digest"]
== manifest_states.get(row["path"])
for row in journal["targets"]
)
if not committed:
restore_bundle(repository, bundle, journal)
restored += 1
cleanup_bundle(bundle)
return len(bundles), restored
@contextmanager
def publication_lock(path: Path, timeout_seconds: float) -> Iterator[None]:
if not path.is_file() or path.is_symlink():
raise PublicationError(
"publication lock_ref must name an existing regular contract file"
)
descriptor = os.open(path, os.O_RDONLY)
deadline = time.monotonic() + timeout_seconds
try:
while True:
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
if time.monotonic() >= deadline:
raise PublicationError(
f"publication lock timed out after {timeout_seconds:g}s"
)
time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
yield
finally:
try:
fcntl.flock(descriptor, fcntl.LOCK_UN)
finally:
os.close(descriptor)
@contextmanager
def handled_signals() -> Iterator[None]:
previous: dict[int, Any] = {}
def interrupt(signum: int, _frame: Any) -> None:
raise PublicationInterrupted(f"publication interrupted by signal {signum}")
for signum in (signal.SIGINT, signal.SIGTERM):
previous[signum] = signal.getsignal(signum)
signal.signal(signum, interrupt)
try:
yield
finally:
for signum, handler in previous.items():
signal.signal(signum, handler)
def prepare_target(
stage: Path,
repository: Path,
target: dict[str, Any],
transaction_id: str,
index: int,
) -> Path | None:
rendered = resolve_ref(stage, target["path"])
if not os.path.lexists(rendered):
return None
live = resolve_ref(repository, target["path"])
prepared = live.parent / f".ao-publish-new-{transaction_id}-{index}"
if os.path.lexists(prepared):
remove_path(prepared)
copy_path_exact(rendered, prepared)
fsync_tree(prepared)
return prepared
def replace_target(
live: Path,
prepared: Path | None,
transaction_id: str,
index: int,
) -> Path | None:
old = live.parent / f".ao-publish-old-{transaction_id}-{index}"
if os.path.lexists(old):
remove_path(old)
if os.path.lexists(live):
os.replace(live, old)
fsync_directory(live.parent)
if prepared is not None:
os.replace(prepared, live)
fsync_directory(live.parent)
return old if os.path.lexists(old) else None
def remove_asides(paths: list[Path]) -> None:
for path in paths:
if os.path.lexists(path):
remove_path(path)
fsync_directory(path.parent)
def repository_input_digest(repository: Path, targets: list[dict[str, Any]]) -> str:
excluded = [target["path"] for target in targets]
return digest_value(scan_tree(repository, excluded_roots=excluded))
def receipt(
*,
mode: str,
result: str,
owner_map_ref: str,
owner_map_digest: str,
input_digest: str,
before_digest: str,
rendered_digest: str,
after_digest: str,
rows: list[dict[str, Any]],
manifest_ref: str | None,
manifest_digest: str | None,
recovery_found: int,
recovery_restored: int,
) -> dict[str, Any]:
return finalize_artifact(
{
"schema_version": RECEIPT_SCHEMA,
"mode": mode,
"result": result,
"owner_map_ref": owner_map_ref,
"owner_map_digest": owner_map_digest,
"input_digest": input_digest,
"before_digest": before_digest,
"rendered_digest": rendered_digest,
"after_digest": after_digest,
"classifications": [
{
"owner": row["owner"],
"path": row["path"],
"classification": row["classification"],
"before_state_digest": row["before"]["state_digest"],
"rendered_state_digest": row["rendered"]["state_digest"],
}
for row in rows
],
"publication_manifest_ref": manifest_ref,
"publication_manifest_digest": manifest_digest,
"recovery": {
"pending_transactions_found": recovery_found,
"restored_transactions": recovery_restored,
},
"checked": [
"exclusive publication lock",
"strict owner-map identity and non-overlap",
"complete isolated staged render",
"declared-owner staged mutation containment",
"second-render byte and mode idempotence",
"exact before, rendered, and after filesystem identities",
"unowned collision refusal before live mutation",
"filesystem recovery bundle independent of Git",
"publication manifest written after owned targets",
],
"observed_at": datetime.now(timezone.utc)
.isoformat()
.replace("+00:00", "Z"),
}
)
def store_receipt(receipt_value: dict[str, Any], destination: Path) -> Path:
destination.mkdir(parents=True, exist_ok=True)
target = destination / f"{receipt_value['artifact_digest']}.json"
payload = canonical_bytes(receipt_value) + b"\n"
if target.exists():
if target.read_bytes() != payload:
raise PublicationError(f"receipt integrity collision: {target}")
return target
atomic_write_bytes(target, payload)
return target
def publish(
repository: Path,
owner_map_ref: str,
owner_map: dict[str, Any],
*,
mode: str,
fail_after_target: int | None,
abrupt_after_target: int | None,
) -> tuple[dict[str, Any], Path | None]:
targets = all_targets(owner_map)
publication = owner_map["publication"]
manifest_ref = publication["manifest_ref"]
manifest_path = resolve_ref(
repository,
manifest_ref,
allow_leaf_symlink=False,
)
transaction_root = resolve_ref(
repository,
publication["transaction_dir_ref"],
allow_leaf_symlink=False,
)
receipt_root = resolve_ref(
repository,
publication["receipt_dir_ref"],
allow_leaf_symlink=False,
)
recovery_found, recovery_restored = recover_pending(
repository,
transaction_root,
owner_map,
allow_mutation=mode in {"WRITE", "RECOVER"},
)
if mode == "RECOVER":
empty_digest = digest_value([])
value = receipt(
mode="RECOVER",
result="RECOVERED",
owner_map_ref=owner_map_ref,
owner_map_digest=owner_map["artifact_digest"],
input_digest=repository_input_digest(repository, targets),
before_digest=empty_digest,
rendered_digest=empty_digest,
after_digest=empty_digest,
rows=[],
manifest_ref=manifest_ref if manifest_path.exists() else None,
manifest_digest=(
load_publication_manifest(manifest_path)["artifact_digest"]
if manifest_path.exists()
else None
),
recovery_found=recovery_found,
recovery_restored=recovery_restored,
)
path = store_receipt(value, receipt_root)
return value, path
prior_manifest = load_publication_manifest(manifest_path)
if prior_manifest is not None:
expected_bindings = [
(target["owner"], target["path"], target["kind"]) for target in targets
]
observed_bindings = [
(row["owner"], row["path"], row["declared_kind"])
for row in prior_manifest["targets"]
]
if (
prior_manifest["owner_map_digest"] != owner_map["artifact_digest"]
or observed_bindings != expected_bindings
):
raise PublicationError(
"publication manifest does not bind the current owner map"
)
input_digest = repository_input_digest(repository, targets)
temporary, stage, rendered_rows = render_complete(repository, owner_map)
try:
rows = []
for rendered_row in rendered_rows:
before = snapshot_path(resolve_ref(repository, rendered_row["path"]))
classification = classify_target(
rendered_row,
before,
rendered_row["rendered"],
prior_manifest,
)
if classification not in CLASSIFICATIONS:
raise PublicationError("internal classification error")
rows.append(
{
**rendered_row,
"before": before,
"classification": classification,
}
)
before_digest = aggregate_states(rows, "before")
rendered_digest = aggregate_states(rows, "rendered")
if any(row["classification"] == "UNOWNED" for row in rows):
collisions = [
row["path"] for row in rows if row["classification"] == "UNOWNED"
]
raise PublicationError(
"unowned projection collision before mutation: " + ", ".join(collisions)
)
clean = all(
row["before"]["state_digest"] == row["rendered"]["state_digest"]
for row in rows
)
if mode == "CHECK":
value = receipt(
mode="CHECK",
result="CLEAN" if clean else "DRIFT",
owner_map_ref=owner_map_ref,
owner_map_digest=owner_map["artifact_digest"],
input_digest=input_digest,
before_digest=before_digest,
rendered_digest=rendered_digest,
after_digest=before_digest,
rows=rows,
manifest_ref=manifest_ref if prior_manifest else None,
manifest_digest=(
prior_manifest["artifact_digest"] if prior_manifest else None
),
recovery_found=recovery_found,
recovery_restored=recovery_restored,
)
return value, None
if (
clean
and prior_manifest is not None
and prior_manifest["owner_map_digest"] == owner_map["artifact_digest"]
and prior_manifest["input_digest"] == input_digest
and prior_manifest["rendered_digest"] == rendered_digest
):
value = receipt(
mode="WRITE",
result="IDEMPOTENT",
owner_map_ref=owner_map_ref,
owner_map_digest=owner_map["artifact_digest"],
input_digest=input_digest,
before_digest=before_digest,
rendered_digest=rendered_digest,
after_digest=before_digest,
rows=rows,
manifest_ref=manifest_ref,
manifest_digest=prior_manifest["artifact_digest"],
recovery_found=recovery_found,
recovery_restored=recovery_restored,
)
return value, store_receipt(value, receipt_root)
transaction_id = uuid.uuid4().hex
bundle, journal = build_recovery_bundle(
repository,
transaction_root,
transaction_id,
rows,
manifest_ref,
owner_map["artifact_digest"],
)
asides: list[Path] = []
committed = False
try:
with handled_signals():
for index, row in enumerate(rows, start=1):
prepared = prepare_target(
stage,
repository,
row,
transaction_id,
index,
)
aside = replace_target(
resolve_ref(repository, row["path"]),
prepared,
transaction_id,
index,
)
if aside is not None:
asides.append(aside)
if fail_after_target == index:
raise PublicationError(f"injected failure after target {index}")
if abrupt_after_target == index:
os._exit(97)
after_rows = [
{
**row,
"after": snapshot_path(resolve_ref(repository, row["path"])),
}
for row in rows
]
for row in after_rows:
if row["after"]["state_digest"] != row["rendered"]["state_digest"]:
raise PublicationError(
f"published bytes differ from staging: {row['path']}"
)
manifest = finalize_artifact(
{
"schema_version": PUBLICATION_SCHEMA,
"transaction_id": transaction_id,
"owner_map_digest": owner_map["artifact_digest"],
"input_digest": input_digest,
"rendered_digest": rendered_digest,
"targets": [
{
"owner": row["owner"],
"path": row["path"],
"declared_kind": row["kind"],
"state": row["after"],
}
for row in after_rows
],
"published_at": datetime.now(timezone.utc)
.isoformat()
.replace("+00:00", "Z"),
}
)
atomic_write_json(manifest_path, manifest)
committed = True
remove_asides(asides)
cleanup_bundle(bundle)
except BaseException:
if not committed:
restore_bundle(repository, bundle, journal)
remove_asides(asides)
cleanup_bundle(bundle)
raise
final_rows = [
{
**row,
"after": snapshot_path(resolve_ref(repository, row["path"])),
}
for row in rows
]
after_digest = aggregate_states(final_rows, "after")
manifest = load_publication_manifest(manifest_path)
assert manifest is not None
value = receipt(
mode="WRITE",
result="PUBLISHED",
owner_map_ref=owner_map_ref,
owner_map_digest=owner_map["artifact_digest"],
input_digest=input_digest,
before_digest=before_digest,
rendered_digest=rendered_digest,
after_digest=after_digest,
rows=rows,
manifest_ref=manifest_ref,
manifest_digest=manifest["artifact_digest"],
recovery_found=recovery_found,
recovery_restored=recovery_restored,
)
return value, store_receipt(value, receipt_root)
finally:
temporary.cleanup()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--repository",
default=".",
help="repository root containing the owner map and generator inputs",
)
parser.add_argument(
"--owner-map",
default="docs/contracts/generated-projection-owners.v1.json",
)
modes = parser.add_mutually_exclusive_group(required=True)
modes.add_argument("--check", action="store_true")
modes.add_argument("--write", action="store_true")
modes.add_argument("--recover-only", action="store_true")
modes.add_argument("--validate-owner-map", action="store_true")
parser.add_argument("--lock-timeout", type=float, default=30.0)
parser.add_argument("--fail-after-target", type=int)
parser.add_argument("--abrupt-after-target", type=int)
return parser.parse_args()
def main() -> int:
args = parse_args()
repository = Path(args.repository).resolve()
try:
if not repository.is_dir():
raise PublicationError(f"repository is not a directory: {repository}")
owner_map_ref = normalize_ref(args.owner_map)
owner_map_path = resolve_ref(repository, owner_map_ref)
owner_map = load_json(owner_map_path, OWNER_MAP_SCHEMA)
validate_owner_map(owner_map, repository)
if args.validate_owner_map:
print(
json.dumps(
{
"owner_map_digest": owner_map["artifact_digest"],
"owners": len(owner_map["owners"]),
"result": "PASS",
"targets": len(all_targets(owner_map)),
},
sort_keys=True,
)
)
return 0
if args.lock_timeout < 0:
raise PublicationError("--lock-timeout must be nonnegative")
mode = "CHECK" if args.check else "RECOVER" if args.recover_only else "WRITE"
lock_path = resolve_ref(
repository,
owner_map["publication"]["lock_ref"],
allow_leaf_symlink=False,
)
with publication_lock(lock_path, args.lock_timeout):
locked_owner_map = load_json(owner_map_path, OWNER_MAP_SCHEMA)
validate_owner_map(locked_owner_map, repository)
if locked_owner_map["artifact_digest"] != owner_map["artifact_digest"]:
raise PublicationError(
"owner map changed while acquiring publication lock"
)
owner_map = locked_owner_map
value, receipt_path = publish(
repository,
owner_map_ref,
owner_map,
mode=mode,
fail_after_target=args.fail_after_target,
abrupt_after_target=args.abrupt_after_target,
)
classification_counts = {
classification: sum(
1
for row in value["classifications"]
if row["classification"] == classification
)
for classification in sorted(CLASSIFICATIONS)
}
output = {
"artifact_digest": value["artifact_digest"],
"classification_counts": classification_counts,
"drift_paths": [
row["path"]
for row in value["classifications"]
if row["before_state_digest"] != row["rendered_state_digest"]
],
"mode": value["mode"],
"publication_manifest_digest": value["publication_manifest_digest"],
"receipt_path": (
receipt_path.relative_to(repository).as_posix()
if receipt_path is not None
else None
),
"rendered_digest": value["rendered_digest"],
"result": value["result"],
}
print(json.dumps(output, sort_keys=True))
if mode == "CHECK" and value["result"] != "CLEAN":
return 1
return 0
except (OSError, PublicationError, subprocess.SubprocessError) as exc:
print(f"publish-generated-projections: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())