Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/SANITIZED_ARTIFACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ compression are fixed. Approval binds reviewer, time, policy, derivative tree,
manifest, archive SHA-256, and archive byte size. `push` verifies all hashes and
sends that exact ZIP without reconstructing it.

The approval record lands in `<derivative>/.openadapt-approval.json`, outside the
archive, because it binds that archive's own SHA-256. Unzipping
`<derivative>.approved.zip` somewhere else gives you the reviewed bytes without
the review, and `compile`, `validate-hosted`, and `push` all read that copy as
unapproved. To move an approved derivative, copy the whole directory, hidden
approval file included, next to its sibling ZIP.

## Destination trust

Execution lane and egress destination are independent:
Expand Down
11 changes: 10 additions & 1 deletion openadapt_flow/runtime_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,13 +406,22 @@ def create_runtime_validation_attestation(
)
challenge = _validate_challenge(challenge)

# Both derivatives fail the same checks with the same wording. Name the
# flag that carried the failing path: an operator who has just approved one
# of the two otherwise reads the other one's refusal as that approval
# failing.
try:
recording_manifest = load_and_verify_derivative(recording_derivative)
recording_approval = load_valid_approval(recording_derivative)
except SanitizationError as exc:
raise RuntimeValidationError(
f"--recording {recording_derivative}: {exc}"
) from exc
try:
bundle_manifest = load_and_verify_derivative(bundle_derivative)
bundle_approval = load_valid_approval(bundle_derivative)
except SanitizationError as exc:
raise RuntimeValidationError(str(exc)) from exc
raise RuntimeValidationError(f"--bundle {bundle_derivative}: {exc}") from exc
if recording_manifest.get("kind") != "recording":
raise RuntimeValidationError("Validation source must be a recording derivative")
if bundle_manifest.get("kind") != "bundle":
Expand Down
16 changes: 15 additions & 1 deletion openadapt_flow/sanitized_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -1159,8 +1159,22 @@ def load_valid_approval(destination: Path) -> dict[str, Any]:
manifest = load_and_verify_derivative(destination)
path = approval_path(destination)
if not path.is_file():
archive = approved_archive_path(destination)
if archive.is_file():
# The approval binds the archive's SHA-256, so `approve-sanitized`
# never writes the approval into the archive it is hashing.
# Extracting that archive therefore rebuilds the reviewed bytes
# without the review.
raise SanitizationError(
f"{destination} has not been approved: {APPROVAL_NAME} is missing "
f"even though {archive.name} is beside it. Extracting an approved "
"archive does not restore its approval. Copy the reviewed "
f"directory with its {APPROVAL_NAME}, or review and approve this "
"copy here."
)
raise SanitizationError(
"Sanitized artifact has not been approved. Review it locally, then approve it."
f"{destination} has not been approved (no {APPROVAL_NAME}). Review it "
"locally, then approve it."
)
try:
approval = json.loads(path.read_text(encoding="utf-8"))
Expand Down
76 changes: 75 additions & 1 deletion tests/test_runtime_validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import zipfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
Expand Down Expand Up @@ -30,7 +31,14 @@
request_validation_challenge,
verify_runtime_validation_attestation,
)
from openadapt_flow.sanitized_artifact import approve_derivative, sanitize_artifact
from openadapt_flow.sanitized_artifact import (
APPROVAL_NAME,
approval_path,
approve_derivative,
approved_archive_path,
load_and_verify_derivative,
sanitize_artifact,
)

_TARGET_URL = "https://mockmed.example.com/login"
_TARGET_ORIGIN = "https://mockmed.example.com"
Expand Down Expand Up @@ -830,3 +838,69 @@ def test_unrelated_recording_or_same_name_report_cannot_attest(tmp_path):
if key != "bundle_derivative"
},
)


def test_recording_materialized_by_extracting_its_approved_archive_is_named(tmp_path):
"""A hosted requalification runner extracts ``<derivative>.approved.zip``.

The approval record is deliberately absent from that archive, so the
extracted tree is unapproved. The refusal must name the recording role, the
exact path, and the extraction that produced it; otherwise the operator
reads a bundle they just approved successfully into a recording failure.
"""
recording, bundle, run_dir = _approved_artifacts(tmp_path)

materialized = tmp_path / "runner" / "recording"
materialized.parent.mkdir(parents=True)
archive = approved_archive_path(recording)
with zipfile.ZipFile(archive) as source_archive:
assert APPROVAL_NAME not in source_archive.namelist()
source_archive.extractall(materialized)
approved_archive_path(materialized).write_bytes(archive.read_bytes())

# The extracted tree is a valid derivative; only its approval is absent.
assert load_and_verify_derivative(materialized)["kind"] == "recording"
assert not approval_path(materialized).is_file()

with pytest.raises(RuntimeValidationError) as excinfo:
create_runtime_validation_attestation(
recording_derivative=materialized,
bundle_derivative=bundle,
run_dir=run_dir,
policy_source="permissive",
risk_class="low",
environment="local-test/mockmed-v1",
target_url=_TARGET_URL,
host=hosted.DEFAULT_HOST,
token="oai_ingest_test",
challenge=_challenge(),
)
message = str(excinfo.value)
assert "--recording" in message
assert str(materialized) in message
assert APPROVAL_NAME in message
assert approved_archive_path(materialized).name in message
assert "has not been approved" in message


def test_bundle_approval_failure_names_the_bundle_flag(tmp_path):
recording, bundle, run_dir = _approved_artifacts(tmp_path)
approval_path(bundle).unlink()

with pytest.raises(RuntimeValidationError) as excinfo:
create_runtime_validation_attestation(
recording_derivative=recording,
bundle_derivative=bundle,
run_dir=run_dir,
policy_source="permissive",
risk_class="low",
environment="local-test/mockmed-v1",
target_url=_TARGET_URL,
host=hosted.DEFAULT_HOST,
token="oai_ingest_test",
challenge=_challenge(),
)
message = str(excinfo.value)
assert "--bundle" in message
assert str(bundle) in message
assert "--recording" not in message