"""Bounded sandbox Git return into an already-owned repository transaction. Artifact bytes stay process-local and are discarded on any gateway/teardown failure. They never enter GatewayResult, Hub, metrics or the close outbox. The initial contract supports one local commit, matching the factory pilot. """ from __future__ import annotations import base64 import binascii import hashlib import json import os import re import subprocess import tempfile from pathlib import Path from typing import Any from rein_aharness.execution_cancel import ExecutionCancel, resolve_cancel from rein_aharness.repository_grant import RepositoryGrant from rein_aharness.repository_transaction import ( RepositoryTransaction, RepositoryTransactionError, ) MAX_BUNDLE_BYTES = 128 * 1024 MAX_NEW_OBJECT_BYTES = 2 * 1024 * 1024 _OID = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}") class RepositoryArtifactError(RepositoryTransactionError): """A sandbox result cannot be safely imported under this transaction.""" # Executed inside the selected sandbox by its owner, with no host path access. # The owner transport separately caps output and wall time. No binary or source # content is placed in command arguments or exception messages. _EXPORT = r""" import base64, json, pathlib, subprocess, sys, tempfile repo, baseline, expected, limit = sys.argv[1:] limit = int(limit) def git(*args): p = subprocess.run(["git", "-C", repo, "-c", "core.hooksPath=/dev/null", *args], capture_output=True, timeout=20) if p.returncode: raise RuntimeError("sandbox artifact Git operation refused") return p.stdout if git("rev-parse", "HEAD").decode().strip() != expected: raise RuntimeError("sandbox artifact head mismatch") if git("status", "--porcelain=v1", "--untracked-files=all").strip(): raise RuntimeError("sandbox artifact checkout is not clean") if git("rev-list", "--count", baseline + "..HEAD").strip() != b"1": raise RuntimeError("sandbox artifact must contain one commit") git("merge-base", "--is-ancestor", baseline, "HEAD") with tempfile.TemporaryDirectory(prefix="rein-artifact-") as d: path = pathlib.Path(d) / "return.bundle" git("bundle", "create", str(path), baseline + "..HEAD") if path.stat().st_size > limit: raise RuntimeError("sandbox artifact too large") payload = path.read_bytes() print(json.dumps({"version": "1", "baseline": baseline, "head": expected, "bundle": base64.b64encode(payload).decode("ascii")})) """ def _git(repo: Path, *args: str) -> bytes: # Do not permit inherited Git directory/object/config overrides to redirect # the transfer. All transport is between owner-selected local paths. env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} env.update( GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, GIT_TERMINAL_PROMPT="0", GIT_NO_REPLACE_OBJECTS="1", ) try: proc = subprocess.run( [ "git", "-C", str(repo), "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "protocol.allow=never", "-c", "protocol.file.allow=always", *args, ], env=env, capture_output=True, timeout=30, ) except (OSError, subprocess.TimeoutExpired) as exc: raise RepositoryArtifactError( "artifact Git operation unavailable or timed out" ) from exc if proc.returncode: raise RepositoryArtifactError("artifact Git validation or import refused") return proc.stdout class RepositoryArtifactTransfer: """Trusted worker-owned capture; source mutation waits for successful teardown.""" def __init__( self, transaction: RepositoryTransaction, grant: RepositoryGrant, *, cancel: ExecutionCancel | None = None, ) -> None: if ( not isinstance(transaction, RepositoryTransaction) or not transaction.locked or transaction.baseline is None ): raise RepositoryArtifactError( "artifact transfer requires the active repository transaction" ) if grant.min_commits != 1 or grant.max_commits != 1 or grant.publish: raise RepositoryArtifactError( "artifact transfer v1 requires exactly one local commit" ) self.transaction = transaction self.grant = grant self.cancel = resolve_cancel(cancel) self._bundle: bytes | None = None self._head: str | None = None def _check(self) -> None: if self.cancel is not None: self.cancel.check() self.transaction.assert_baseline_unchanged() def capture(self, sandbox: Any, summary: Any) -> None: """Called by Glas after session cleanup, before sandbox destruction.""" from glas_harness.transport import transport_from_sandbox self._check() if self._bundle is not None: raise RepositoryArtifactError("artifact capture was already completed") head = summary.commit_sha if not isinstance(head, str) or not _OID.fullmatch(head): raise RepositoryArtifactError( "sandbox did not report an exact commit identity" ) transport = transport_from_sandbox(sandbox) if transport.kind != "local_namespace": raise RepositoryArtifactError( "artifact transfer v1 requires bounded sandbox owner execution" ) result = transport.run( [ "python3", "-c", _EXPORT, transport.workspace, self.transaction.baseline.head, head, str(MAX_BUNDLE_BYTES), ], timeout=30, ) if result.returncode: raise RepositoryArtifactError("sandbox artifact export refused") if len(result.stdout) > (MAX_BUNDLE_BYTES * 4 // 3) + 1024: raise RepositoryArtifactError("sandbox artifact exceeds transport limit") try: payload = json.loads(result.stdout) if ( not isinstance(payload, dict) or set(payload) != {"version", "baseline", "head", "bundle"} or payload["version"] != "1" or payload["baseline"] != self.transaction.baseline.head or payload["head"] != head ): raise ValueError("artifact envelope mismatch") bundle = base64.b64decode(payload["bundle"], validate=True) if not bundle or len(bundle) > MAX_BUNDLE_BYTES: raise ValueError("artifact byte limit") except (ValueError, TypeError, binascii.Error) as exc: raise RepositoryArtifactError( "sandbox artifact envelope is invalid" ) from exc self._bundle, self._head = bundle, head self._check() def import_after_teardown(self, reported_head: str | None) -> dict[str, Any]: """Validate in a disposable checkout, then fast-forward under the live lease.""" self._check() if self._bundle is None or self._head is None or reported_head != self._head: raise RepositoryArtifactError( "successful gateway did not return the captured artifact" ) baseline = self.transaction.baseline with tempfile.TemporaryDirectory(prefix="rein-artifact-verify-") as temporary: root = Path(temporary) bundle_path = root / "return.bundle" bundle_path.write_bytes(self._bundle) bundle_path.chmod(0o600) staging = root / "checkout" _git( root, "clone", "--quiet", "--no-local", "--no-hardlinks", "--no-checkout", "--", str(baseline.repo_root), str(staging), ) _git(staging, "checkout", "--quiet", "--detach", baseline.head) with RepositoryTransaction(staging, state_dir=root / "state") as inspection: _git(staging, "bundle", "verify", str(bundle_path)) advertised = ( _git(staging, "bundle", "list-heads", str(bundle_path)) .decode() .strip() ) if advertised != self._head + " HEAD": raise RepositoryArtifactError( "artifact must advertise only the expected HEAD" ) _git( staging, "fetch", "--quiet", "--no-tags", "--no-write-fetch-head", str(bundle_path), "HEAD", ) if ( _git( staging, "rev-list", "--count", f"{baseline.head}..{self._head}" ).strip() != b"1" ): raise RepositoryArtifactError( "artifact contains an unexpected commit count" ) _git(staging, "merge-base", "--is-ancestor", baseline.head, self._head) # Reject symlinks and gitlinks before checking out untrusted data. changes = _git( staging, "diff", "--raw", "--no-renames", "-z", baseline.head, self._head, "--", ).split(b"\0") for header in changes[::2]: if header and header.split()[1] not in ( b"100644", b"100755", b"000000", ): raise RepositoryArtifactError( "artifact changes an unsupported file mode" ) oids = _git( staging, "rev-list", "--objects", "--no-object-names", f"{baseline.head}..{self._head}", ).splitlines() if len(oids) > 1024: raise RepositoryArtifactError( "artifact contains too many new objects" ) total = 0 for oid in oids: total += int(_git(staging, "cat-file", "-s", oid.decode()).strip()) if total > MAX_NEW_OBJECT_BYTES: raise RepositoryArtifactError( "artifact expands beyond the accepted byte limit" ) _git( staging, "merge", "--ff-only", "--no-edit", "--no-stat", self._head ) inspection.validate_acceptance(self.grant.acceptance_policy()) self._check() _git( baseline.repo_root, "fetch", "--quiet", "--no-tags", "--no-write-fetch-head", str(bundle_path), "HEAD", ) self._check() _git( baseline.repo_root, "merge", "--ff-only", "--no-edit", "--no-stat", self._head, ) if self.cancel is not None: self.cancel.check() accepted = self.transaction.validate_acceptance( self.grant.acceptance_policy() ) evidence = { "version": "1", "sha256": hashlib.sha256(self._bundle).hexdigest(), "bytes": len(self._bundle), "head": accepted.head, "imported": True, } self._bundle = None return evidence