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
6 changes: 3 additions & 3 deletions .github/workflows/harness-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.28"

Expand Down Expand Up @@ -54,7 +54,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.28"

Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.28"

Expand Down
55 changes: 5 additions & 50 deletions template/scripts/_vendor_loop_schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,6 @@
# Vendored from brunovicco/engineering-loop-schemas
# @ 75a63eef269fd995128ab39c89e551fe58a27bf7 (v0.1.0, includes the UP037/RUF100 lint fixes: a
# per-file-ignore for the load-bearing quoted return annotations, and
# removal of a stray noqa that was itself flagged once RUF is enabled).
# No published tag exists yet as of this vendoring; pinned to the commit
# above. Do not edit by hand -- re-vendor from the source repository
# instead. See docs/LOOPS.md for how this fits into the Phase 0-1
# report-only loop foundation.
# Adapted for vendoring: this directory is named `_vendor_loop_schemas`
# (not `loop_schemas`) so it does not itself match the `scripts/loop_*`
# out-of-scope-loop-path denylist in protect_sensitive_files.py; the
# `from loop_schemas...` imports were changed to
# `from _vendor_loop_schemas...` to match. No other lines were edited.
"""Evidence-Gated Engineering Loop schemas: models and a dependency-free validator.

This package is the canonical, shared contract between every engineering-loop
implementation (currently the Codex and claude-python-engineering-harness
Python engineering harnesses). It defines the shape of a loop `contract`,
the mechanical `evidence` a run produces, the `verdict` derived from grading that evidence, and the
non-authoritative `builder-result` a builder reports.

Everything here is Phase 0-1, report-only: no code in this package executes a
loop, promotes a candidate, or grants any agent the authority to certify its
own work. See README.md for the full model and the documented final states.
"""

from _vendor_loop_schemas.models import (
Acceptance,
Actions,
Baseline,
Budgets,
Contract,
HumanReview,
Scope,
Selection,
Trigger,
)

__all__ = [
"Acceptance",
"Actions",
"Baseline",
"Budgets",
"Contract",
"HumanReview",
"Scope",
"Selection",
"Trigger",
]

__version__ = "0.1.0"
# Version: 0.1.2
# Commit: 0459d61b7b1d4e7b46709e6d3895770553e6fab0
# Generated by scripts/render_vendor_bundle.py; do not edit by hand.
# Phase 0-1 remains report-only: this bundle validates contracts only.
"""Vendored validators for Evidence-Gated Engineering Loop contracts."""
29 changes: 29 additions & 0 deletions template/scripts/_vendor_loop_schemas/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"adaptations": [
{
"file": "validate_contract.py",
"from": "from loop_schemas.models import Contract",
"to": "from _vendor_loop_schemas.models import Contract"
}
],
"files": {
"__init__.py": {
"sha256": "37a1ec4b860278188c30e64a66399a75a6f66257631db8012bffaa707aedc8b4",
"size_bytes": 333
},
"models.py": {
"sha256": "0683ca41aad2b185570b5d85ba6953fd6bcbfcd45930cda75983531c675f0b4e",
"size_bytes": 10785
},
"validate_contract.py": {
"sha256": "f089b0d2ca642c5048ed9965729c614036a65bfea764ddaeaf022bcd6fe2e672",
"size_bytes": 7323
}
},
"manifest_version": "1.0.0",
"source": {
"commit": "0459d61b7b1d4e7b46709e6d3895770553e6fab0",
"repository": "brunovicco/engineering-loop-schemas",
"version": "0.1.2"
}
}
16 changes: 4 additions & 12 deletions template/scripts/_vendor_loop_schemas/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
# Vendored from brunovicco/engineering-loop-schemas
# @ 75a63eef269fd995128ab39c89e551fe58a27bf7 (v0.1.0, includes the UP037/RUF100 lint fixes: a
# per-file-ignore for the load-bearing quoted return annotations, and
# removal of a stray noqa that was itself flagged once RUF is enabled).
# No published tag exists yet as of this vendoring; pinned to the commit
# above. Do not edit by hand -- re-vendor from the source repository
# instead. See docs/LOOPS.md for how this fits into the Phase 0-1
# report-only loop foundation.
# Adapted for vendoring: this directory is named `_vendor_loop_schemas`
# (not `loop_schemas`) so it does not itself match the `scripts/loop_*`
# out-of-scope-loop-path denylist in protect_sensitive_files.py; the
# `from loop_schemas...` imports were changed to
# `from _vendor_loop_schemas...` to match. No other lines were edited.
# Version: 0.1.2
# Commit: 0459d61b7b1d4e7b46709e6d3895770553e6fab0
# Generated by scripts/render_vendor_bundle.py; do not edit by hand.
# Phase 0-1 remains report-only: this bundle validates contracts only.
"""Typed dataclasses corresponding to schemas/*.schema.json.

Deliberately stdlib-only (dataclasses + typing) so this module -- and
Expand Down
20 changes: 6 additions & 14 deletions template/scripts/_vendor_loop_schemas/validate_contract.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
#!/usr/bin/env python3
# Vendored from brunovicco/engineering-loop-schemas
# @ 75a63eef269fd995128ab39c89e551fe58a27bf7 (v0.1.0, includes the UP037/RUF100 lint fixes: a
# per-file-ignore for the load-bearing quoted return annotations, and
# removal of a stray noqa that was itself flagged once RUF is enabled).
# No published tag exists yet as of this vendoring; pinned to the commit
# above. Do not edit by hand -- re-vendor from the source repository
# instead. See docs/LOOPS.md for how this fits into the Phase 0-1
# report-only loop foundation.
# Adapted for vendoring: this directory is named `_vendor_loop_schemas`
# (not `loop_schemas`) so it does not itself match the `scripts/loop_*`
# out-of-scope-loop-path denylist in protect_sensitive_files.py; the
# `from loop_schemas...` import below was changed to
# `from _vendor_loop_schemas...` to match. No other lines were edited.
# Version: 0.1.2
# Commit: 0459d61b7b1d4e7b46709e6d3895770553e6fab0
# Generated by scripts/render_vendor_bundle.py; do not edit by hand.
# Phase 0-1 remains report-only: this bundle validates contracts only.
"""Validate an Evidence-Gated Engineering Loop contract. CLI and library.

Deliberately stdlib-only so this file (and models.py, which it depends on)
Expand Down Expand Up @@ -40,8 +32,8 @@

from _vendor_loop_schemas.models import Contract

# The hard-gate names both the Codex and claude-python-engineering-harness
# Python engineering harnesses' quality_gate.py scripts already report as named checks
# The hard-gate names shared by both Python engineering harnesses'
# quality_gate.py scripts are already reported as named checks
# (see Check(...) entries in template/scripts/quality_gate.py in both
# harnesses). A hard gate declared in a contract must resolve to a check
# a harness's own quality gate can actually run.
Expand Down
4 changes: 4 additions & 0 deletions template/scripts/quality_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def configured_checks(root: Path) -> list[Check]:
Check("architecture", (sys.executable, "scripts/validate_architecture.py")),
Check("mcp", (sys.executable, "scripts/validate_mcp_config.py")),
Check("governance", (sys.executable, "scripts/governance_gate.py")),
Check(
"loop-schema-vendor",
(sys.executable, "scripts/validate_loop_schema_vendor.py"),
),
Check("loop-contracts", (sys.executable, "scripts/validate_loop_contracts.py")),
]
checks.append(Check("typing", ("mypy", *type_roots) if type_roots else ()))
Expand Down
162 changes: 162 additions & 0 deletions template/scripts/validate_loop_schema_vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Verify the offline integrity and provenance of vendored loop schemas."""

import argparse
import hashlib
import json
import re
import sys
from pathlib import Path
from typing import Any, cast

COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$")
SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
EXPECTED_REPOSITORY = "brunovicco/engineering-loop-schemas"
REQUIRED_FILES = {"__init__.py", "models.py", "validate_contract.py"}


def sha256_file(path: Path) -> str:
"""Return the SHA-256 digest for one file."""
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()


def load_manifest(vendor_dir: Path) -> dict[str, Any]:
"""Load the local provenance manifest."""
path = vendor_dir / "manifest.json"
document = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(document, dict):
raise ValueError("manifest.json must contain an object")
return cast(dict[str, Any], document)


def validate_manifest(vendor_dir: Path) -> list[str]:
"""Return all integrity and provenance errors."""
errors: list[str] = []
try:
manifest = load_manifest(vendor_dir)
except (OSError, json.JSONDecodeError, ValueError) as exc:
return [f"could not load manifest.json: {exc}"]

if manifest.get("manifest_version") != "1.0.0":
errors.append(f"unsupported manifest_version: {manifest.get('manifest_version')!r}")

source = manifest.get("source")
if not isinstance(source, dict):
errors.append("source must be an object")
source = {}

repository = source.get("repository")
version = source.get("version")
commit = source.get("commit")
if repository != EXPECTED_REPOSITORY:
errors.append(f"unexpected source repository: {repository!r}")
if not isinstance(version, str) or not SEMVER_PATTERN.fullmatch(version):
errors.append(f"invalid source version: {version!r}")
if not isinstance(commit, str) or not COMMIT_PATTERN.fullmatch(commit):
errors.append(f"invalid source commit: {commit!r}")

files = manifest.get("files")
if not isinstance(files, dict):
return [*errors, "files must be an object"]

names = set(files)
if names != REQUIRED_FILES:
errors.append("manifest files must be exactly: " + ", ".join(sorted(REQUIRED_FILES)))

for name in sorted(REQUIRED_FILES):
metadata = files.get(name)
if not isinstance(metadata, dict):
errors.append(f"{name}: metadata must be an object")
continue
expected_hash = metadata.get("sha256")
expected_size = metadata.get("size_bytes")
if not isinstance(expected_hash, str) or not SHA256_PATTERN.fullmatch(expected_hash):
errors.append(f"{name}: invalid sha256 in manifest")
continue
if not isinstance(expected_size, int) or expected_size < 0:
errors.append(f"{name}: invalid size_bytes in manifest")
continue

path = vendor_dir / name
if not path.is_file():
errors.append(f"{name}: file is missing")
continue
actual_hash = sha256_file(path)
actual_size = path.stat().st_size
if actual_hash != expected_hash:
errors.append(f"{name}: sha256 mismatch; expected {expected_hash}, got {actual_hash}")
if actual_size != expected_size:
errors.append(f"{name}: size mismatch; expected {expected_size}, got {actual_size}")

validator = vendor_dir / "validate_contract.py"
if validator.is_file():
text = validator.read_text(encoding="utf-8")
adapted = "from _vendor_loop_schemas.models import Contract"
original = "from loop_schemas.models import Contract"
if adapted not in text:
errors.append("validate_contract.py: vendored package import is missing")
if original in text:
errors.append("validate_contract.py: source package import was not adapted")

for name in ("models.py", "validate_contract.py"):
path = vendor_dir / name
if not path.is_file() or not isinstance(version, str) or not isinstance(commit, str):
continue
text = path.read_text(encoding="utf-8")
if f"# Version: {version}" not in text:
errors.append(f"{name}: provenance version header is missing")
if f"# Commit: {commit}" not in text:
errors.append(f"{name}: provenance commit header is missing")

return errors


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="Generated-project root; defaults to the parent of scripts/.",
)
parser.add_argument("--json", action="store_true", dest="as_json")
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
"""Validate the generated project's local vendored bundle."""
args = parse_args(argv)
vendor_dir = args.root.resolve() / "scripts" / "_vendor_loop_schemas"
errors = validate_manifest(vendor_dir)

if args.as_json:
print(
json.dumps(
{
"valid": not errors,
"vendor_dir": str(vendor_dir),
"errors": errors,
},
indent=2,
sort_keys=True,
)
)
elif errors:
print("Vendored loop-schema integrity check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
else:
manifest = load_manifest(vendor_dir)
source = cast(dict[str, Any], manifest["source"])
print(f"Vendored loop-schema bundle is valid: {source['version']} @ {source['commit']}.")
return 1 if errors else 0


if __name__ == "__main__":
raise SystemExit(main())
49 changes: 49 additions & 0 deletions tests/test_loop_schema_vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Regression tests for the vendored loop-schema integrity gate."""

import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
VALIDATOR = ROOT / "template" / "scripts" / "validate_loop_schema_vendor.py"
VENDOR = ROOT / "template" / "scripts" / "_vendor_loop_schemas"


class LoopSchemaVendorIntegrityTests(unittest.TestCase):
"""The generated template must ship a verified, tamper-evident bundle."""

def run_validator(self, root: Path) -> subprocess.CompletedProcess[str]:
"""Run the validator against one synthetic generated-project root."""
return subprocess.run(
[sys.executable, str(VALIDATOR), "--root", str(root)],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)

def test_template_bundle_is_valid(self) -> None:
"""The canonical template bundle matches its local manifest."""
result = self.run_validator(ROOT / "template")
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)

def test_tampering_is_detected(self) -> None:
"""Changing a vendored source file must fail the gate."""
with tempfile.TemporaryDirectory() as directory:
project = Path(directory)
target = project / "scripts" / "_vendor_loop_schemas"
target.parent.mkdir(parents=True)
shutil.copytree(VENDOR, target)
with (target / "models.py").open("a", encoding="utf-8") as handle:
handle.write("\n# unauthorized local edit\n")

result = self.run_validator(project)
self.assertEqual(result.returncode, 1)
self.assertIn("sha256 mismatch", result.stderr)


if __name__ == "__main__":
unittest.main()