Some checks are pending
Governed runtime contract / contract (push) Waiting to run
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""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
|