From 751e19b6a9f303c5f26ea30ff6a99cdc8e1a4a44 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:27:13 +0200 Subject: [PATCH 1/6] exec: add sealed public execution worker --- scripts/sealed_public_execution.py | 227 +++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 scripts/sealed_public_execution.py diff --git a/scripts/sealed_public_execution.py b/scripts/sealed_public_execution.py new file mode 100644 index 0000000..50a4e35 --- /dev/null +++ b/scripts/sealed_public_execution.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Execute a bounded public-safe capsule and return only sealed evidence. + +The capsule is a gzip-compressed tar archive passed as base64. It must contain +`run.sh`. Task stdout/stderr and files written beneath SEALED_RESULT_DIR are +captured into the plaintext result bundle, encrypted with age to the caller's +recipient, then the plaintext is removed. + +This worker deliberately owns no project semantics. The trusted/private side is +responsible for deciding whether a projection is public-safe before dispatch. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import re +import shutil +import subprocess +import tarfile +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +ASSIGNMENT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,95}$") +AGE_RECIPIENT_RE = re.compile(r"^age1[0-9a-z]+$") +MAX_CAPSULE_B64 = 60_000 +MAX_MEMBER_COUNT = 256 +MAX_UNPACKED_BYTES = 16 * 1024 * 1024 +MAX_TIMEOUT_SECONDS = 7_200 + + +class WorkerError(ValueError): + pass + + +def _sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def _utc_now() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _validate_assignment_id(value: str) -> str: + if not ASSIGNMENT_RE.fullmatch(value): + raise WorkerError("assignment_id must be 3-96 safe identifier characters") + return value + + +def _validate_recipient(value: str) -> str: + if not AGE_RECIPIENT_RE.fullmatch(value): + raise WorkerError("recipient must be an age X25519 recipient (age1...)") + return value + + +def decode_capsule(encoded: str, expected_sha256: str, destination: Path) -> Path: + if not encoded or len(encoded) > MAX_CAPSULE_B64: + raise WorkerError(f"capsule_b64 must be 1-{MAX_CAPSULE_B64} characters") + if not re.fullmatch(r"[A-Za-z0-9+/=\r\n]+", encoded): + raise WorkerError("capsule_b64 contains non-base64 characters") + if not re.fullmatch(r"[0-9a-f]{64}", expected_sha256): + raise WorkerError("capsule_sha256 must be a lowercase SHA-256 hex digest") + try: + raw = base64.b64decode(encoded, validate=True) + except Exception as exc: # binascii.Error varies by Python version + raise WorkerError("capsule_b64 is not valid base64") from exc + capsule = destination / "capsule.tar.gz" + capsule.write_bytes(raw) + actual = _sha256(capsule) + if actual != expected_sha256: + raise WorkerError("capsule SHA-256 mismatch") + return capsule + + +def safe_extract(capsule: Path, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=False) + total = 0 + with tarfile.open(capsule, mode="r:gz") as tf: + members = tf.getmembers() + if not members or len(members) > MAX_MEMBER_COUNT: + raise WorkerError("capsule member count is outside the allowed bound") + root = destination.resolve() + for member in members: + if member.issym() or member.islnk() or member.isdev(): + raise WorkerError("capsule links/devices are prohibited") + total += max(member.size, 0) + if total > MAX_UNPACKED_BYTES: + raise WorkerError("capsule exceeds unpacked-size limit") + resolved = (destination / member.name).resolve() + try: + resolved.relative_to(root) + except ValueError as exc: + raise WorkerError("capsule path escapes execution directory") from exc + tf.extractall(destination, members=members, filter="data") + run_sh = destination / "run.sh" + if not run_sh.is_file() or run_sh.is_symlink(): + raise WorkerError("capsule must contain a regular top-level run.sh") + + +def package_result(source: Path, destination: Path) -> None: + with tarfile.open(destination, mode="w:gz") as tf: + for item in sorted(source.rglob("*")): + if item.is_file() and not item.is_symlink(): + tf.add(item, arcname=item.relative_to(source), recursive=False) + + +def seal_result(plaintext: Path, recipient: str, ciphertext: Path) -> None: + if shutil.which("age") is None: + raise WorkerError("age executable is required") + subprocess.run( + ["age", "--encrypt", "--recipient", recipient, "--output", str(ciphertext), str(plaintext)], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + + +def run_assignment( + *, assignment_id: str, capsule_b64: str, capsule_sha256: str, + recipient: str, timeout_seconds: int, out_dir: Path, +) -> int: + assignment_id = _validate_assignment_id(assignment_id) + recipient = _validate_recipient(recipient) + if timeout_seconds < 1 or timeout_seconds > MAX_TIMEOUT_SECONDS: + raise WorkerError(f"timeout_seconds must be 1-{MAX_TIMEOUT_SECONDS}") + + started_at = _utc_now() + out_dir.mkdir(parents=True, exist_ok=False) + with tempfile.TemporaryDirectory(prefix="sealed-public-exec-") as temp_name: + temp = Path(temp_name) + capsule = decode_capsule(capsule_b64, capsule_sha256, temp) + work = temp / "work" + safe_extract(capsule, work) + result = temp / "result" + result.mkdir() + stdout_path = result / "stdout.txt" + stderr_path = result / "stderr.txt" + + env = os.environ.copy() + env.update({ + "SEALED_ASSIGNMENT_ID": assignment_id, + "SEALED_RESULT_DIR": str(result / "files"), + }) + (result / "files").mkdir() + exit_code = 125 + timed_out = False + with stdout_path.open("wb") as stdout_fh, stderr_path.open("wb") as stderr_fh: + try: + completed = subprocess.run( + ["bash", "run.sh"], cwd=work, env=env, + stdout=stdout_fh, stderr=stderr_fh, + timeout=timeout_seconds, check=False, + ) + exit_code = int(completed.returncode) + except subprocess.TimeoutExpired: + exit_code = 124 + timed_out = True + + metadata = { + "schema_version": 1, + "assignment_id": assignment_id, + "started_at": started_at, + "ended_at": _utc_now(), + "exit_code": exit_code, + "timed_out": timed_out, + "capsule_sha256": capsule_sha256, + "worker_repository": os.getenv("GITHUB_REPOSITORY"), + "worker_revision": os.getenv("GITHUB_SHA"), + "run_id": os.getenv("GITHUB_RUN_ID"), + "run_attempt": os.getenv("GITHUB_RUN_ATTEMPT"), + } + (result / "execution.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8") + + plaintext = temp / "result.tar.gz" + package_result(result, plaintext) + ciphertext = out_dir / "result.age" + seal_result(plaintext, recipient, ciphertext) + plaintext.unlink(missing_ok=True) + + receipt = { + "schema_version": 1, + "assignment_id": assignment_id, + "status": "completed" if exit_code == 0 else "failed", + "sealed_sha256": _sha256(ciphertext), + "sealed_bytes": ciphertext.stat().st_size, + "worker_revision": os.getenv("GITHUB_SHA"), + "run_id": os.getenv("GITHUB_RUN_ID"), + "run_attempt": os.getenv("GITHUB_RUN_ATTEMPT"), + } + (out_dir / "receipt.json").write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8") + return exit_code + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--assignment-id", required=True) + parser.add_argument("--capsule-b64", required=True) + parser.add_argument("--capsule-sha256", required=True) + parser.add_argument("--recipient", required=True) + parser.add_argument("--timeout-seconds", type=int, default=3600) + parser.add_argument("--out-dir", default=".sealed") + args = parser.parse_args() + try: + return run_assignment( + assignment_id=args.assignment_id, + capsule_b64=args.capsule_b64, + capsule_sha256=args.capsule_sha256, + recipient=args.recipient, + timeout_seconds=args.timeout_seconds, + out_dir=Path(args.out_dir), + ) + except WorkerError as exc: + print(f"sealed public execution rejected: {exc}", file=os.sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 4457d32a7ef93b74d3f44334db750d88bd6d02d6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:27:26 +0200 Subject: [PATCH 2/6] exec: add sealed public Actions lane --- .github/workflows/sealed-public-execution.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/sealed-public-execution.yml diff --git a/.github/workflows/sealed-public-execution.yml b/.github/workflows/sealed-public-execution.yml new file mode 100644 index 0000000..b4200ce --- /dev/null +++ b/.github/workflows/sealed-public-execution.yml @@ -0,0 +1,88 @@ +name: Sealed Public Execution +run-name: Sealed execution ${{ inputs.assignment_id }} + +on: + workflow_dispatch: + inputs: + assignment_id: + description: "Opaque assignment correlation ID" + required: true + type: string + capsule_b64: + description: "Base64 gzip-tar execution capsule; public-safe projection only" + required: true + type: string + capsule_sha256: + description: "Lowercase SHA-256 of decoded capsule bytes" + required: true + type: string + recipient: + description: "Ephemeral age X25519 public recipient (age1...)" + required: true + type: string + timeout_seconds: + description: "Task timeout, max 7200 seconds" + required: false + default: "3600" + type: string + +permissions: + contents: read + +jobs: + execute: + runs-on: ubuntu-latest + timeout-minutes: 125 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install result sealer + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq age >/dev/null + + - name: Execute public-safe capsule and seal evidence + id: execute + shell: bash + env: + ASSIGNMENT_ID: ${{ inputs.assignment_id }} + CAPSULE_B64: ${{ inputs.capsule_b64 }} + CAPSULE_SHA256: ${{ inputs.capsule_sha256 }} + AGE_RECIPIENT: ${{ inputs.recipient }} + TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }} + run: | + set +e + python3 scripts/sealed_public_execution.py \ + --assignment-id "$ASSIGNMENT_ID" \ + --capsule-b64 "$CAPSULE_B64" \ + --capsule-sha256 "$CAPSULE_SHA256" \ + --recipient "$AGE_RECIPIENT" \ + --timeout-seconds "$TIMEOUT_SECONDS" \ + --out-dir .sealed + rc=$? + echo "task_rc=$rc" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Upload sealed result mailbox + if: always() && hashFiles('.sealed/result.age', '.sealed/receipt.json') != '' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: sealed-${{ inputs.assignment_id }} + path: | + .sealed/result.age + .sealed/receipt.json + if-no-files-found: error + retention-days: 1 + compression-level: 0 + + - name: Preserve task verdict + if: always() + shell: bash + env: + TASK_RC: ${{ steps.execute.outputs.task_rc }} + run: | + test -n "$TASK_RC" || exit 1 + exit "$TASK_RC" From f6b9e09751652810ded86181b4d0b1f425d35e10 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:27:44 +0200 Subject: [PATCH 3/6] test: cover sealed execution capsule boundary --- tests/test_sealed_public_execution.py | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_sealed_public_execution.py diff --git a/tests/test_sealed_public_execution.py b/tests/test_sealed_public_execution.py new file mode 100644 index 0000000..e3df11f --- /dev/null +++ b/tests/test_sealed_public_execution.py @@ -0,0 +1,90 @@ +import base64 +import hashlib +import io +import tarfile +import tempfile +import unittest +from pathlib import Path + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) + +import sealed_public_execution as worker + + +def make_capsule(entries): + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz") as tf: + for name, content, kind in entries: + info = tarfile.TarInfo(name) + if kind == "file": + data = content.encode() + info.size = len(data) + info.mode = 0o644 + tf.addfile(info, io.BytesIO(data)) + elif kind == "symlink": + info.type = tarfile.SYMTYPE + info.linkname = content + tf.addfile(info) + return buffer.getvalue() + + +class SealedExecutionContractTests(unittest.TestCase): + def test_assignment_ids_are_bounded(self): + self.assertEqual(worker._validate_assignment_id("rep-001"), "rep-001") + with self.assertRaises(worker.WorkerError): + worker._validate_assignment_id("../escape") + + def test_age_recipient_shape_is_required(self): + self.assertEqual(worker._validate_recipient("age1qqqqqqqq"), "age1qqqqqqqq") + with self.assertRaises(worker.WorkerError): + worker._validate_recipient("not-a-recipient") + + def test_decode_requires_matching_digest(self): + raw = make_capsule([("run.sh", "echo ok\n", "file")]) + encoded = base64.b64encode(raw).decode() + with tempfile.TemporaryDirectory() as td: + path = worker.decode_capsule(encoded, hashlib.sha256(raw).hexdigest(), Path(td)) + self.assertEqual(path.read_bytes(), raw) + with self.assertRaises(worker.WorkerError): + worker.decode_capsule(encoded, "0" * 64, Path(td)) + + def test_safe_extract_accepts_top_level_runner(self): + raw = make_capsule([ + ("run.sh", "mkdir -p \"$SEALED_RESULT_DIR\"; echo ok > \"$SEALED_RESULT_DIR/out.txt\"\n", "file"), + ("probe.json", "{}\n", "file"), + ]) + with tempfile.TemporaryDirectory() as td: + root = Path(td) + capsule = root / "capsule.tar.gz" + capsule.write_bytes(raw) + worker.safe_extract(capsule, root / "work") + self.assertTrue((root / "work" / "run.sh").is_file()) + + def test_safe_extract_rejects_traversal(self): + raw = make_capsule([ + ("run.sh", "echo ok\n", "file"), + ("../escape", "bad\n", "file"), + ]) + with tempfile.TemporaryDirectory() as td: + root = Path(td) + capsule = root / "capsule.tar.gz" + capsule.write_bytes(raw) + with self.assertRaises(worker.WorkerError): + worker.safe_extract(capsule, root / "work") + + def test_safe_extract_rejects_links(self): + raw = make_capsule([ + ("run.sh", "echo ok\n", "file"), + ("link", "../outside", "symlink"), + ]) + with tempfile.TemporaryDirectory() as td: + root = Path(td) + capsule = root / "capsule.tar.gz" + capsule.write_bytes(raw) + with self.assertRaises(worker.WorkerError): + worker.safe_extract(capsule, root / "work") + + +if __name__ == "__main__": + unittest.main() From a52c799f21ca7ff928b0318f29a6077c2f61e84f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:27:53 +0200 Subject: [PATCH 4/6] ci: validate sealed execution contract --- .../sealed-public-execution-contract.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/sealed-public-execution-contract.yml diff --git a/.github/workflows/sealed-public-execution-contract.yml b/.github/workflows/sealed-public-execution-contract.yml new file mode 100644 index 0000000..0421be3 --- /dev/null +++ b/.github/workflows/sealed-public-execution-contract.yml @@ -0,0 +1,25 @@ +name: Sealed Public Execution Contract + +on: + pull_request: + paths: + - 'scripts/sealed_public_execution.py' + - 'tests/test_sealed_public_execution.py' + - '.github/workflows/sealed-public-execution.yml' + - '.github/workflows/sealed-public-execution-contract.yml' + - 'docs/sealed-public-execution.md' + workflow_dispatch: + +permissions: + contents: read + +jobs: + contract: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Run dependency-free contract tests + run: python3 -m unittest -v tests/test_sealed_public_execution.py From 26f1dc023ac7e6986733f4a131f8db79577ff39b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:28:12 +0200 Subject: [PATCH 5/6] docs: define sealed public execution boundary --- docs/sealed-public-execution.md | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/sealed-public-execution.md diff --git a/docs/sealed-public-execution.md b/docs/sealed-public-execution.md new file mode 100644 index 0000000..eee2937 --- /dev/null +++ b/docs/sealed-public-execution.md @@ -0,0 +1,75 @@ +# Sealed public execution + +Status: experimental public-safe execution adapter. + +## Purpose + +Use standard GitHub-hosted Actions capacity in this public repository for work that a trusted/private authority has already determined is safe to project into a public runner, while keeping substantive result evidence out of Git history and returning it to the trusted side as short-lived ciphertext. + +This adapter is an execution/materialization mechanism only. It does not own project intent, acceptance truth, research state, task prioritization, or declassification decisions. + +## Boundary + +Trusted/private side responsibilities: + +1. resolve the originating project-native authority; +2. determine whether the work may execute on a public runner; +3. derive the least-sufficient public-safe capsule; +4. generate a per-assignment age X25519 keypair and retain the private identity outside GitHub; +5. dispatch the public workflow with only the opaque assignment ID, capsule, capsule digest, public recipient, and bounded timeout; +6. retrieve the encrypted Actions artifact, verify the receipt/ciphertext digest, decrypt privately, and reconcile useful evidence back to the originating authority; +7. delete the public artifact/run when it no longer has diagnostic value. + +Public runner responsibilities: + +1. verify the capsule digest and strict archive bounds; +2. execute only the capsule's top-level `run.sh` with no private credentials; +3. capture task stdout/stderr into the private result bundle rather than Actions logs; +4. collect files written beneath `SEALED_RESULT_DIR`; +5. package and encrypt the result to the supplied age recipient; +6. upload only `result.age` plus a minimal `receipt.json` as a one-day Actions artifact; +7. preserve task success/failure in the workflow verdict. + +## Capsule contract + +The workflow accepts a gzip-compressed tar archive encoded as base64. Current hard bounds are intentionally small: + +- base64 input: at most 60,000 characters; +- archive entries: at most 256; +- unpacked content: at most 16 MiB; +- no symlinks, hardlinks, devices, or path traversal; +- a regular top-level `run.sh` is required; +- task timeout is at most 7,200 seconds. + +At runtime the worker sets: + +- `SEALED_ASSIGNMENT_ID` — opaque correlation identity; +- `SEALED_RESULT_DIR` — directory for substantive result files. + +The capsule itself is public-observable execution material. Do not put information in it that the originating authority has not approved for public-runner exposure. + +## Result contract + +The public artifact contains only: + +- `result.age` — age-encrypted gzip tar containing execution metadata, captured stdout/stderr, and result files; +- `receipt.json` — opaque assignment ID, completed/failed status, ciphertext SHA-256/size, public worker revision, and Actions run correlation. + +Retention is set to one day. Private reconciliation should normally delete the artifact sooner after successful pickup/decryption. + +The artifact is transport, not durable project authority. Durable conclusions, accepted evidence, negative results, or follow-on decisions belong back in the originating private/project-native authority. + +## Security / value-preservation invariants + +- No private credential is required by the execution workflow. +- No private repository checkout occurs in this lane. +- The private decryption identity never enters GitHub. +- No plaintext result artifact is uploaded. +- Task stdout/stderr are not intentionally emitted to Actions logs. +- Public runner visibility of the projected capsule is accepted by the originating authority before dispatch. +- The public repository must not accumulate experiment interpretation, private hypotheses, result corpora, or project-specific research history merely because it provided compute. +- A second project does not need to adopt this adapter unless its own authority chooses to use it. + +## Non-goals + +This increment does not add a scheduler, task database, queue, generic provider registry, automatic public/private classifier, result archive, or new portfolio authority. It also does not make `agent-dispatch` mandatory for interactive/local execution paths. From 3a195d2d0ef5cbfa43ff43cc8dcf37e7d64606d9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:29:55 +0200 Subject: [PATCH 6/6] ci: exercise sealed execution round trip --- .../sealed-public-execution-contract.yml | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sealed-public-execution-contract.yml b/.github/workflows/sealed-public-execution-contract.yml index 0421be3..503e0ba 100644 --- a/.github/workflows/sealed-public-execution-contract.yml +++ b/.github/workflows/sealed-public-execution-contract.yml @@ -16,10 +16,62 @@ permissions: jobs: contract: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 8 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Run dependency-free contract tests run: python3 -m unittest -v tests/test_sealed_public_execution.py + + - name: Install age for round-trip test + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq age >/dev/null + + - name: Exercise execute-seal-decrypt round trip + shell: bash + run: | + set -euo pipefail + mkdir -p /tmp/sealed-selftest/capsule + cat > /tmp/sealed-selftest/capsule/run.sh <<'SH' + echo "captured stdout" + echo "captured stderr" >&2 + printf 'round-trip-ok\n' > "$SEALED_RESULT_DIR/out.txt" + SH + tar -C /tmp/sealed-selftest/capsule -czf /tmp/sealed-selftest/capsule.tar.gz run.sh + capsule_sha=$(sha256sum /tmp/sealed-selftest/capsule.tar.gz | awk '{print $1}') + capsule_b64=$(base64 -w0 /tmp/sealed-selftest/capsule.tar.gz) + + age-keygen -o /tmp/sealed-selftest/identity.txt >/dev/null 2>&1 + recipient=$(awk '/# public key:/ {print $4}' /tmp/sealed-selftest/identity.txt) + test -n "$recipient" + + python3 scripts/sealed_public_execution.py \ + --assignment-id selftest-001 \ + --capsule-b64 "$capsule_b64" \ + --capsule-sha256 "$capsule_sha" \ + --recipient "$recipient" \ + --timeout-seconds 30 \ + --out-dir /tmp/sealed-selftest/sealed + + age --decrypt \ + --identity /tmp/sealed-selftest/identity.txt \ + --output /tmp/sealed-selftest/result.tar.gz \ + /tmp/sealed-selftest/sealed/result.age + mkdir /tmp/sealed-selftest/result + tar -C /tmp/sealed-selftest/result -xzf /tmp/sealed-selftest/result.tar.gz + + grep -qx 'captured stdout' /tmp/sealed-selftest/result/stdout.txt + grep -qx 'captured stderr' /tmp/sealed-selftest/result/stderr.txt + grep -qx 'round-trip-ok' /tmp/sealed-selftest/result/files/out.txt + python3 - <<'PY' + import json + from pathlib import Path + receipt = json.loads(Path('/tmp/sealed-selftest/sealed/receipt.json').read_text()) + assert receipt['assignment_id'] == 'selftest-001' + assert receipt['status'] == 'completed' + assert len(receipt['sealed_sha256']) == 64 + PY