Close staging preflight review gaps

This commit is contained in:
fwazb 2026-07-20 23:43:10 -07:00
parent 16b32193f7
commit 2f6cfeaaa4
4 changed files with 257 additions and 22 deletions

View file

@ -12,7 +12,11 @@ architecture, private-only VM attachment to `teleo-staging-europe-west6`
(`10.60.0.0/20`), Private Google Access, immutable Docker Artifact (`10.60.0.0/20`), Private Google Access, immutable Docker Artifact
Registry repository, exact private PostgreSQL 16 Cloud SQL endpoint, host Registry repository, exact private PostgreSQL 16 Cloud SQL endpoint, host
architecture, Docker server, Docker storage floor, unused service/container architecture, Docker server, Docker storage floor, unused service/container
names, and private TCP/5432 reachability from the VM. names, and private TCP/5432 reachability from the VM. Before the first Docker
client command, it requires `docker.service` to be loaded, active, and running;
after the final client command it requires unchanged service state, `MainPID`,
and restart count. This prevents a read probe from activating Docker through
`docker.socket` or silently accepting a daemon restart.
## Authority boundary ## Authority boundary
@ -42,10 +46,13 @@ python3 ops/check_gcp_leoclean_nosend_preflight.py \
The output parent must already exist, be owned by the operator, and have no The output parent must already exist, be owned by the operator, and have no
group/other write permission. The output name must not exist. The checker group/other write permission. The output name must not exist. The checker
creates it once with no-clobber/no-symlink semantics, keeps the same file and creates it once with no-clobber/no-symlink semantics, keeps the same file and
directory identity open through publication, writes mode `0600`, and fsyncs directory identity open through publication, writes and fsyncs a mode-`0600`
the file and directory. It never removes or overwrites the output pathname; private staged inode, and only then hard-links that complete inode to the
an ambiguous publication failure can therefore leave a private residue for canonical output name without replacement. The retained staged and canonical
operator inspection. names are two links to the same inode; they are never removed automatically.
A pre-publication failure therefore leaves only the private staged residue,
not a valid-looking canonical pass receipt. A later ambiguous publication
failure preserves the exact retained inode for operator inspection.
Authentication, permission, connection, and receipt-publication failures emit Authentication, permission, connection, and receipt-publication failures emit
only a bounded category; command stderr and filesystem exception details are only a bounded category; command stderr and filesystem exception details are

View file

@ -73,19 +73,22 @@ cut over safely and retire the VPS after soak.
bounded IAP/SSH path uses a fixed remote command and a maximum five-minute bounded IAP/SSH path uses a fixed remote command and a maximum five-minute
key lifetime. Receipt publication now requires a pre-existing private key lifetime. Receipt publication now requires a pre-existing private
operator-owned directory, creates one mode-`0600` no-clobber/no-symlink operator-owned directory, creates one mode-`0600` no-clobber/no-symlink
file through a held directory descriptor, verifies stable identity, fsyncs staged inode through a held directory descriptor, fsyncs it before a
both file and directory, and preserves ambiguous residues. No live run is no-clobber canonical hard link, and retains both links without automatic
currently authorized. cleanup. The host probe proves Docker loaded/active/running before its first
client call and unchanged service state, `MainPID`, and restart count after
the last, preventing socket activation from changing runtime state. No live
run is currently authorized.
- Artifact Registry cleanup policy is not an additional hard gate for the - Artifact Registry cleanup policy is not an additional hard gate for the
package: immutable tags prevent deletion of the retained tagged candidate. package: immutable tags prevent deletion of the retained tagged candidate.
The post-push/finalization path must still re-prove the exact retained The post-push/finalization path must still re-prove the exact retained
candidate tag-to-digest binding immediately before deployment. candidate tag-to-digest binding immediately before deployment.
- Current offline evidence for the preflight extraction: 243 focused - Current offline evidence for the preflight extraction: 250 focused
preflight/package/OCI tests pass; Ruff, Python compilation, formatting, and preflight/package/OCI tests pass; Ruff, Python compilation, formatting, and
diff checks pass. The repository suite produced 2,497 passes and 5 expected diff checks pass. The repository suite produced 2,504 passes and 5 expected
skips inside the desktop sandbox; all 18 sandbox-denied Docker, loopback, and skips inside the desktop sandbox; all 18 sandbox-denied Docker, loopback, and
Swift-cache cases passed in a separately authorized 59-test local rerun, Swift-cache cases passed in a separately authorized 59-test local rerun,
reconciling the suite to 2,515 passes and 5 skips. No cloud or registry reconciling the suite to 2,522 passes and 5 skips. No cloud or registry
endpoint was contacted. endpoint was contacted.
- Historical only: the superseded preflight branch records an earlier live - Historical only: the superseded preflight branch records an earlier live
observation of `e2-standard-4` on `teleo-staging-europe-west6` observation of `e2-standard-4` on `teleo-staging-europe-west6`

View file

@ -14,6 +14,7 @@ import shlex
import stat import stat
import subprocess import subprocess
import sys import sys
import uuid
from collections.abc import Callable from collections.abc import Callable
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
@ -397,6 +398,16 @@ project=\"$(metadata project/project-id)\"
instance=\"$(metadata instance/name)\" instance=\"$(metadata instance/name)\"
service_account=\"$(metadata instance/service-accounts/default/email)\" service_account=\"$(metadata instance/service-accounts/default/email)\"
test -x /usr/bin/docker test -x /usr/bin/docker
docker_service_load_state_before=\"$(systemctl show docker.service --property=LoadState --value)\"
docker_service_active_state_before=\"$(systemctl show docker.service --property=ActiveState --value)\"
docker_service_sub_state_before=\"$(systemctl show docker.service --property=SubState --value)\"
docker_service_main_pid_before=\"$(systemctl show docker.service --property=MainPID --value)\"
docker_service_n_restarts_before=\"$(systemctl show docker.service --property=NRestarts --value)\"
test \"$docker_service_load_state_before\" = loaded
test \"$docker_service_active_state_before\" = active
test \"$docker_service_sub_state_before\" = running
test \"$docker_service_main_pid_before\" -gt 1
test \"$docker_service_n_restarts_before\" -ge 0
docker_server_version=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Version}}}}')\" docker_server_version=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Version}}}}')\"
docker_server_os=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Os}}}}')\" docker_server_os=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Os}}}}')\"
docker_server_arch=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Arch}}}}')\" docker_server_arch=\"$(sudo -n /usr/bin/docker version --format '{{{{.Server.Arch}}}}')\"
@ -404,11 +415,26 @@ docker_root=\"$(sudo -n /usr/bin/docker info --format '{{{{.DockerRootDir}}}}')\
docker_root_free_bytes=\"$(sudo -n /usr/bin/df -B1 --output=avail \"$docker_root\" | tail -n 1 | tr -d ' ')\" docker_root_free_bytes=\"$(sudo -n /usr/bin/df -B1 --output=avail \"$docker_root\" | tail -n 1 | tr -d ' ')\"
unit_load_state=\"$(systemctl show {shlex.quote(package.SERVICE)} --property=LoadState --value)\" unit_load_state=\"$(systemctl show {shlex.quote(package.SERVICE)} --property=LoadState --value)\"
container_collision_count=\"$(sudo -n /usr/bin/docker ps -aq --filter 'name=^/{package.CONTAINER_NAME}$' | wc -l | tr -d ' ')\" container_collision_count=\"$(sudo -n /usr/bin/docker ps -aq --filter 'name=^/{package.CONTAINER_NAME}$' | wc -l | tr -d ' ')\"
docker_service_load_state_after=\"$(systemctl show docker.service --property=LoadState --value)\"
docker_service_active_state_after=\"$(systemctl show docker.service --property=ActiveState --value)\"
docker_service_sub_state_after=\"$(systemctl show docker.service --property=SubState --value)\"
docker_service_main_pid_after=\"$(systemctl show docker.service --property=MainPID --value)\"
docker_service_n_restarts_after=\"$(systemctl show docker.service --property=NRestarts --value)\"
test \"$docker_service_load_state_after\" = \"$docker_service_load_state_before\"
test \"$docker_service_active_state_after\" = \"$docker_service_active_state_before\"
test \"$docker_service_sub_state_after\" = \"$docker_service_sub_state_before\"
test \"$docker_service_main_pid_after\" = \"$docker_service_main_pid_before\"
test \"$docker_service_n_restarts_after\" = \"$docker_service_n_restarts_before\"
if timeout 5 bash -c 'exec 3<>/dev/tcp/{private_ip}/5432; exec 3<&-; exec 3>&-'; then cloudsql_tcp=reachable; else cloudsql_tcp=unreachable; fi if timeout 5 bash -c 'exec 3<>/dev/tcp/{private_ip}/5432; exec 3<&-; exec 3>&-'; then cloudsql_tcp=reachable; else cloudsql_tcp=unreachable; fi
printf 'architecture=%s\n' \"$architecture\" printf 'architecture=%s\n' \"$architecture\"
printf 'project=%s\n' \"$project\" printf 'project=%s\n' \"$project\"
printf 'instance=%s\n' \"$instance\" printf 'instance=%s\n' \"$instance\"
printf 'service_account=%s\n' \"$service_account\" printf 'service_account=%s\n' \"$service_account\"
printf 'docker_service_load_state=%s\n' \"$docker_service_load_state_after\"
printf 'docker_service_active_state=%s\n' \"$docker_service_active_state_after\"
printf 'docker_service_sub_state=%s\n' \"$docker_service_sub_state_after\"
printf 'docker_service_main_pid=%s\n' \"$docker_service_main_pid_after\"
printf 'docker_service_n_restarts=%s\n' \"$docker_service_n_restarts_after\"
printf 'docker_server_version=%s\n' \"$docker_server_version\" printf 'docker_server_version=%s\n' \"$docker_server_version\"
printf 'docker_server_os=%s\n' \"$docker_server_os\" printf 'docker_server_os=%s\n' \"$docker_server_os\"
printf 'docker_server_arch=%s\n' \"$docker_server_arch\" printf 'docker_server_arch=%s\n' \"$docker_server_arch\"
@ -448,6 +474,11 @@ def _parse_host_output(output: str) -> dict[str, str]:
"project", "project",
"instance", "instance",
"service_account", "service_account",
"docker_service_load_state",
"docker_service_active_state",
"docker_service_sub_state",
"docker_service_main_pid",
"docker_service_n_restarts",
"docker_server_version", "docker_server_version",
"docker_server_os", "docker_server_os",
"docker_server_arch", "docker_server_arch",
@ -477,8 +508,15 @@ def _host_readback(runner: Runner, private_ip: str) -> dict[str, Any]:
try: try:
free_bytes = int(value["docker_root_free_bytes"]) free_bytes = int(value["docker_root_free_bytes"])
collision_count = int(value["container_collision_count"]) collision_count = int(value["container_collision_count"])
docker_main_pid = int(value["docker_service_main_pid"])
docker_n_restarts = int(value["docker_service_n_restarts"])
except ValueError as exc: except ValueError as exc:
raise PreflightError("invalid_readback", "host numeric field was invalid") from exc raise PreflightError("invalid_readback", "host numeric field was invalid") from exc
_require(value["docker_service_load_state"] == "loaded", "Docker service is not loaded")
_require(value["docker_service_active_state"] == "active", "Docker service is not active")
_require(value["docker_service_sub_state"] == "running", "Docker service is not running")
_require(docker_main_pid > 1, "Docker service MainPID is invalid")
_require(docker_n_restarts >= 0, "Docker service restart count is invalid")
_require(free_bytes >= MINIMUM_DOCKER_FREE_BYTES, "Docker storage has insufficient free space") _require(free_bytes >= MINIMUM_DOCKER_FREE_BYTES, "Docker storage has insufficient free space")
_require(value["unit_load_state"] == "not-found", "staging service unit name already exists") _require(value["unit_load_state"] == "not-found", "staging service unit name already exists")
_require(collision_count == 0, "staging container name already exists") _require(collision_count == 0, "staging container name already exists")
@ -491,6 +529,14 @@ def _host_readback(runner: Runner, private_ip: str) -> dict[str, Any]:
"service_account": package.SERVICE_ACCOUNT, "service_account": package.SERVICE_ACCOUNT,
}, },
"docker": { "docker": {
"service": {
"load_state": "loaded",
"active_state": "active",
"sub_state": "running",
"main_pid": docker_main_pid,
"n_restarts": docker_n_restarts,
"stable_across_client_observation": True,
},
"server_version": value["docker_server_version"], "server_version": value["docker_server_version"],
"server_os": "linux", "server_os": "linux",
"server_architecture": "amd64", "server_architecture": "amd64",
@ -535,6 +581,8 @@ def build_preflight(*, runner: Runner = _default_runner, source_revision: str) -
"database_credentials_used": False, "database_credentials_used": False,
"database_queries_run": False, "database_queries_run": False,
"control_plane_observations_before_ssh_are_describe_only": True, "control_plane_observations_before_ssh_are_describe_only": True,
"docker_service_proven_active_before_client_calls": True,
"docker_service_identity_stable_after_client_calls": True,
"remote_host_runtime_mutations": False, "remote_host_runtime_mutations": False,
"ssh_access_may_register_time_bounded_key": True, "ssh_access_may_register_time_bounded_key": True,
"ssh_key_maximum_lifetime": SSH_KEY_LIFETIME, "ssh_key_maximum_lifetime": SSH_KEY_LIFETIME,
@ -642,6 +690,37 @@ def _write_all(descriptor: int, payload: bytes) -> None:
offset += written offset += written
def _verify_receipt_entry(
parent_descriptor: int,
name: str,
expected_identity: tuple[int, int],
*,
expected_size: int,
expected_links: int,
label: str,
) -> os.stat_result:
try:
observed = os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
except OSError as exc:
raise PreflightError(
"receipt_publication_failed",
f"{label} cannot be identified safely",
) from exc
if not (
stat.S_ISREG(observed.st_mode)
and (observed.st_dev, observed.st_ino) == expected_identity
and observed.st_uid == os.geteuid()
and stat.S_IMODE(observed.st_mode) == 0o600
and observed.st_nlink == expected_links
and observed.st_size == expected_size
):
raise PreflightError(
"receipt_publication_failed",
f"{label} identity or posture drifted",
)
return observed
def _publish_private_receipt( def _publish_private_receipt(
output_path: Path, output_path: Path,
parent_descriptor: int, parent_descriptor: int,
@ -656,6 +735,7 @@ def _publish_private_receipt(
) from exc ) from exc
descriptor: int | None = None descriptor: int | None = None
staged_name = f".{output_path.name}.{uuid.uuid4().hex}.tmp"
try: try:
_assert_private_output_parent(parent_descriptor, output_path.parent) _assert_private_output_parent(parent_descriptor, output_path.parent)
if _output_entry_stat(parent_descriptor, output_path.name) is not None: if _output_entry_stat(parent_descriptor, output_path.name) is not None:
@ -666,19 +746,16 @@ def _publish_private_receipt(
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"): if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW flags |= os.O_NOFOLLOW
descriptor = os.open(output_path.name, flags, 0o600, dir_fd=parent_descriptor) descriptor = os.open(staged_name, flags, 0o600, dir_fd=parent_descriptor)
os.fchmod(descriptor, 0o600) os.fchmod(descriptor, 0o600)
before = os.fstat(descriptor) before = os.fstat(descriptor)
identity = (before.st_dev, before.st_ino)
_write_all(descriptor, encoded) _write_all(descriptor, encoded)
os.fsync(descriptor) os.fsync(descriptor)
after = os.fstat(descriptor) after = os.fstat(descriptor)
named = os.stat(output_path.name, dir_fd=parent_descriptor, follow_symlinks=False)
through_path = os.stat(output_path, follow_symlinks=False)
if not ( if not (
stat.S_ISREG(after.st_mode) stat.S_ISREG(after.st_mode)
and (before.st_dev, before.st_ino) == (after.st_dev, after.st_ino) and (before.st_dev, before.st_ino) == (after.st_dev, after.st_ino)
and (after.st_dev, after.st_ino) == (named.st_dev, named.st_ino)
and (after.st_dev, after.st_ino) == (through_path.st_dev, through_path.st_ino)
and after.st_uid == os.geteuid() and after.st_uid == os.geteuid()
and stat.S_IMODE(after.st_mode) == 0o600 and stat.S_IMODE(after.st_mode) == 0o600
and after.st_nlink == 1 and after.st_nlink == 1
@ -686,7 +763,44 @@ def _publish_private_receipt(
): ):
raise PreflightError( raise PreflightError(
"receipt_publication_failed", "receipt_publication_failed",
"published preflight receipt identity or posture drifted", "staged preflight receipt identity or posture drifted",
)
_verify_receipt_entry(
parent_descriptor,
staged_name,
identity,
expected_size=len(encoded),
expected_links=1,
label="staged preflight receipt",
)
os.link(
staged_name,
output_path.name,
src_dir_fd=parent_descriptor,
dst_dir_fd=parent_descriptor,
follow_symlinks=False,
)
_verify_receipt_entry(
parent_descriptor,
staged_name,
identity,
expected_size=len(encoded),
expected_links=2,
label="retained staged preflight receipt",
)
_verify_receipt_entry(
parent_descriptor,
output_path.name,
identity,
expected_size=len(encoded),
expected_links=2,
label="published preflight receipt",
)
through_path = os.stat(output_path, follow_symlinks=False)
if (through_path.st_dev, through_path.st_ino) != identity:
raise PreflightError(
"receipt_publication_failed",
"published preflight receipt path identity drifted",
) )
_assert_private_output_parent(parent_descriptor, output_path.parent) _assert_private_output_parent(parent_descriptor, output_path.parent)
os.fsync(parent_descriptor) os.fsync(parent_descriptor)

View file

@ -102,6 +102,11 @@ def live_fixtures() -> dict[str, object]:
"project": "teleo-501523", "project": "teleo-501523",
"instance": "teleo-staging-1", "instance": "teleo-staging-1",
"service_account": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com", "service_account": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com",
"docker_service_load_state": "loaded",
"docker_service_active_state": "active",
"docker_service_sub_state": "running",
"docker_service_main_pid": "1701",
"docker_service_n_restarts": "0",
"docker_server_version": "29.4.0", "docker_server_version": "29.4.0",
"docker_server_os": "linux", "docker_server_os": "linux",
"docker_server_arch": "amd64", "docker_server_arch": "amd64",
@ -164,11 +169,21 @@ def test_exact_read_only_preflight_passes_without_exposing_private_ip() -> None:
"database_credentials_used": False, "database_credentials_used": False,
"database_queries_run": False, "database_queries_run": False,
"control_plane_observations_before_ssh_are_describe_only": True, "control_plane_observations_before_ssh_are_describe_only": True,
"docker_service_proven_active_before_client_calls": True,
"docker_service_identity_stable_after_client_calls": True,
"remote_host_runtime_mutations": False, "remote_host_runtime_mutations": False,
"ssh_access_may_register_time_bounded_key": True, "ssh_access_may_register_time_bounded_key": True,
"ssh_key_maximum_lifetime": "5m", "ssh_key_maximum_lifetime": "5m",
} }
assert payload["host"]["cloudsql_private_tcp_5432"] == "reachable" assert payload["host"]["cloudsql_private_tcp_5432"] == "reachable"
assert payload["host"]["docker"]["service"] == {
"load_state": "loaded",
"active_state": "active",
"sub_state": "running",
"main_pid": 1701,
"n_restarts": 0,
"stable_across_client_observation": True,
}
assert ( assert (
payload["control_plane"]["cloudsql"]["private_ip_sha256"] payload["control_plane"]["cloudsql"]["private_ip_sha256"]
== hashlib.sha256(PRIVATE_IP.encode("ascii")).hexdigest() == hashlib.sha256(PRIVATE_IP.encode("ascii")).hexdigest()
@ -248,7 +263,30 @@ def test_exact_read_only_preflight_passes_without_exposing_private_ip() -> None:
assert "--tunnel-through-iap" in ssh assert "--tunnel-through-iap" in ssh
assert "--ssh-key-expire-after=5m" in ssh assert "--ssh-key-expire-after=5m" in ssh
remote = next(item.removeprefix("--command=") for item in ssh if item.startswith("--command=")) remote = next(item.removeprefix("--command=") for item in ssh if item.startswith("--command="))
syntax = subprocess.run(["/bin/bash", "-n"], input=remote, text=True, capture_output=True, check=False)
assert syntax.returncode == 0, syntax.stderr
assert remote.startswith("set -euo pipefail\n") assert remote.startswith("set -euo pipefail\n")
first_docker_client = remote.index("sudo -n /usr/bin/docker version")
assert remote.index("docker_service_active_state_before=") < first_docker_client
assert remote.index('test "$docker_service_active_state_before" = active') < first_docker_client
assert remote.index('test "$docker_service_sub_state_before" = running') < first_docker_client
assert remote.index('test "$docker_service_main_pid_before" -gt 1') < first_docker_client
last_docker_client = remote.index("sudo -n /usr/bin/docker ps")
assert remote.index("docker_service_active_state_after=", last_docker_client) > last_docker_client
assert (
remote.index(
'test "$docker_service_main_pid_after" = "$docker_service_main_pid_before"',
last_docker_client,
)
> last_docker_client
)
assert (
remote.index(
'test "$docker_service_n_restarts_after" = "$docker_service_n_restarts_before"',
last_docker_client,
)
> last_docker_client
)
for forbidden in ( for forbidden in (
"docker run", "docker run",
"docker pull", "docker pull",
@ -279,6 +317,11 @@ def test_exact_read_only_preflight_passes_without_exposing_private_ip() -> None:
(("artifact", "dockerConfig"), "immutableTags", False, "not immutable"), (("artifact", "dockerConfig"), "immutableTags", False, "not immutable"),
(("cloudsql", "settings", "ipConfiguration"), "ipv4Enabled", True, "public IPv4"), (("cloudsql", "settings", "ipConfiguration"), "ipv4Enabled", True, "public IPv4"),
(("cloudsql",), "databaseVersion", "POSTGRES_160", "exactly PostgreSQL 16"), (("cloudsql",), "databaseVersion", "POSTGRES_160", "exactly PostgreSQL 16"),
(("host",), "docker_service_load_state", "not-found", "not loaded"),
(("host",), "docker_service_active_state", "inactive", "not active"),
(("host",), "docker_service_sub_state", "dead", "not running"),
(("host",), "docker_service_main_pid", "0", "MainPID"),
(("host",), "docker_service_n_restarts", "-1", "restart count"),
(("host",), "container_collision_count", "1", "container name"), (("host",), "container_collision_count", "1", "container name"),
(("host",), "unit_load_state", "loaded", "unit name"), (("host",), "unit_load_state", "loaded", "unit name"),
(("host",), "docker_root_free_bytes", "1024", "free space"), (("host",), "docker_root_free_bytes", "1024", "free space"),
@ -499,7 +542,10 @@ def test_cli_publishes_one_private_no_clobber_receipt(
assert stat.S_ISREG(observed.st_mode) assert stat.S_ISREG(observed.st_mode)
assert stat.S_IMODE(observed.st_mode) == 0o600 assert stat.S_IMODE(observed.st_mode) == 0o600
assert observed.st_uid == os.geteuid() assert observed.st_uid == os.geteuid()
assert observed.st_nlink == 1 assert observed.st_nlink == 2
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert retained[0].stat().st_ino == observed.st_ino
@pytest.mark.parametrize("entry_kind", ["file", "directory", "symlink"]) @pytest.mark.parametrize("entry_kind", ["file", "directory", "symlink"])
@ -578,6 +624,7 @@ def test_publication_race_fails_without_overwriting_the_created_entry(
assert preflight.main(["--output", str(output)]) == 1 assert preflight.main(["--output", str(output)]) == 1
assert output.read_text(encoding="utf-8") == "replacement-sentinel\n" assert output.read_text(encoding="utf-8") == "replacement-sentinel\n"
assert list(output.parent.glob(".preflight.json.*.tmp")) == []
assert json.loads(capsys.readouterr().out) == { assert json.loads(capsys.readouterr().out) == {
"category": "receipt_publication_failed", "category": "receipt_publication_failed",
"schema": preflight.SCHEMA, "schema": preflight.SCHEMA,
@ -585,7 +632,35 @@ def test_publication_race_fails_without_overwriting_the_created_entry(
} }
def test_publication_failure_is_bounded_and_preserves_created_residue( def test_link_race_preserves_the_complete_staged_inode_without_overwrite(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = _private_output_directory(tmp_path) / "preflight.json"
parent_descriptor = preflight._open_private_output_parent(output)
payload = {"schema": preflight.SCHEMA, "status": "pass", "source_revision": REVISION}
real_link = preflight.os.link
def race_link(source: str, destination: str, **kwargs: object) -> None:
output.write_text("replacement-sentinel\n", encoding="utf-8")
real_link(source, destination, **kwargs)
monkeypatch.setattr(preflight.os, "link", race_link)
try:
with pytest.raises(preflight.PreflightError) as caught:
preflight._publish_private_receipt(output, parent_descriptor, payload)
finally:
os.close(parent_descriptor)
assert caught.value.category == "receipt_publication_failed"
assert output.read_text(encoding="utf-8") == "replacement-sentinel\n"
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert json.loads(retained[0].read_text(encoding="utf-8")) == payload
assert retained[0].stat().st_nlink == 1
def test_prepublication_fsync_failure_is_bounded_and_preserves_only_staged_residue(
tmp_path: Path, tmp_path: Path,
capsys: pytest.CaptureFixture[str], capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
@ -611,6 +686,42 @@ def test_publication_failure_is_bounded_and_preserves_created_residue(
"status": "blocked", "status": "blocked",
} }
assert sentinel not in stdout assert sentinel not in stdout
assert output.is_file() assert not output.exists()
assert stat.S_IMODE(output.stat().st_mode) == 0o600 retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert json.loads(output.read_text(encoding="utf-8"))["status"] == "pass" assert len(retained) == 1
assert stat.S_IMODE(retained[0].stat().st_mode) == 0o600
assert retained[0].stat().st_nlink == 1
assert json.loads(retained[0].read_text(encoding="utf-8"))["status"] == "pass"
def test_prepublication_write_failure_is_bounded_and_never_creates_canonical_output(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_successful_cli(monkeypatch)
output = _private_output_directory(tmp_path) / "preflight.json"
sentinel = "private-write-fault-sentinel"
def fail_after_partial_write(descriptor: int, payload: bytes) -> None:
os.write(descriptor, payload[:16])
raise OSError(sentinel)
monkeypatch.setattr(preflight, "_write_all", fail_after_partial_write)
assert preflight.main(["--output", str(output)]) == 1
stdout = capsys.readouterr().out
assert json.loads(stdout) == {
"category": "receipt_publication_failed",
"schema": preflight.SCHEMA,
"status": "blocked",
}
assert sentinel not in stdout
assert not output.exists()
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert stat.S_IMODE(retained[0].stat().st_mode) == 0o600
assert retained[0].stat().st_nlink == 1
with pytest.raises(json.JSONDecodeError):
json.loads(retained[0].read_text(encoding="utf-8"))