diff --git a/AGENTS.md b/AGENTS.md index 6fbfe276..ce6ea847 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ clean develop@SHA -> verified generated tree -> generated-only PR -> main (squas - Copy README.md unchanged from source; use documentation links that work on both branches. - Skills need no build; observer requires Python >=3.10, standard library only, no install-time dependency download. - distribution.json binds source and inventory, not authenticity/acceptance; release_ready=false is not release approval. -- Full workflow, installation probes and promotion gates: [CONTRIBUTING.md](CONTRIBUTING.md). No source license grant: never invent/copy one. +- Full workflow, distribution verification and promotion gates: [CONTRIBUTING.md](CONTRIBUTING.md). No source license grant: never invent/copy one. ## Task truth and restart diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ba95ce3..d7e5e7f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,11 +51,11 @@ contained, locked dependency closure and explicit host prerequisites. 1. Merge an independently reviewed feature PR into develop. 2. Freeze a clean exact source SHA and the expected current main SHA. 3. Build the distribution deterministically, verify its inventory and contained - runtimes, and test Git installation/update in an authorized isolated environment. + runtimes. 4. Obtain independent maintainer approval for that exact source and evidence. 5. Recheck approval, source SHA and current main immediately before a separately authorized generated-tree publication. Changed inputs or stale approval stop it. -6. Publish without force, then verify remote provenance and installation results. +6. Publish without force, then verify remote provenance and the published inventory. Do not use GitHub Merge on a develop-to-main source request. Use squash only for an exact verified generated-tree PR based on previous main. Source changes enter main only @@ -75,20 +75,10 @@ python3 -B tools/production.py smoke /absolute/new/product The candidate binds the exact source commit/tree and rejects dirty or untracked source and uncommitted operational resources. `project` remains a working-tree -preview. Both keep `release_ready=false`: integrity is not review or installation -approval. Verify candidate hashes after isolated Git install/update and before -publication. The opt-in real-host probe uses disposable HOME, XDG and Goose -roots and a local Git transport repository; it never replaces a live installation: - -```sh -python3 -B tests/probe_goose_distribution.py /absolute/new/product --goose /absolute/path/to/goose -``` - -The probe requires Goose 1.56.0, checks the exact seven Skills with -`goose skills list` after installation and update, verifies installed payload -hashes and observer execution, and confirms that update removes an obsolete -fixture. Working-tree previews are diagnostic evidence, not committed-source -or release approval. This probe is not part of the automatic unit suite. +preview. Both keep `release_ready=false`: integrity is not independent review +or release approval. Verify candidate hashes before publication. Candidate, +inventory and runtime checks do not require a Goose executable or installation +into a host environment, and do not establish installed-host compatibility. Production excludes creator eval sets, evaluation/conformance fixtures and synthetic study inputs, including those nested under Skill resources. Unknown diff --git a/tests/probe_goose_distribution.py b/tests/probe_goose_distribution.py deleted file mode 100644 index 787dbe0e..00000000 --- a/tests/probe_goose_distribution.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -"""Opt-in real Goose Git install/update/list probe; disposable roots only. - -Usage: python3 -B tests/probe_goose_distribution.py -Never install into live user paths or publish the temporary Git repository. -""" -import argparse -import hashlib -import importlib.util -import json -import os -from pathlib import Path -import re -import shutil -import subprocess -import sys -import tempfile - -ROOT = Path(__file__).resolve().parents[1] -EXPECTED = sorted(("cycle", "research", "plan", "implement", "test", "judge", "iterate")) - - -def probe(candidate, goose): - spec = importlib.util.spec_from_file_location("production", ROOT / "tools/production.py") - production = importlib.util.module_from_spec(spec) - spec.loader.exec_module(production) - receipt = production.verify(candidate) - production.smoke(candidate) - executable = shutil.which(goose) - if not executable: - raise ValueError("Goose executable unavailable") - with tempfile.TemporaryDirectory(prefix="agentic-development-goose-") as work: - work = Path(work) - repo, home, paths, foreign = (work / name for name in ("repo", "home", "goose", "foreign")) - shutil.copytree(candidate, repo) - home.mkdir() - foreign.mkdir() - env = dict(os.environ, HOME=str(home), GOOSE_PATH_ROOT=str(paths), - XDG_CONFIG_HOME=str(home / ".config"), XDG_DATA_HOME=str(home / ".local/share"), - GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull) - - def run(command, args, cwd=foreign): - result = subprocess.run([command, *args], cwd=cwd, env=env, capture_output=True, text=True, timeout=120) - if result.returncode: - raise ValueError(command + " failed: " + result.stderr) - return result.stdout - - version = run(executable, ["--version"]).strip() - if version != "1.56.0": - raise ValueError("Probe requires verified Goose 1.56.0") - git = lambda *args: run("git", list(args), repo) - git("init", "-q") - git("config", "user.name", "Distribution QA") - git("config", "user.email", "qa@example.invalid") - git("add", "-A") - git("commit", "-qm", "isolated distribution fixture") - run(executable, ["plugin", "install", "--auto-update", repo.as_uri()]) - installed = paths / ".agents/plugins/agentic-development" - - def check(): - listing = run(executable, ["skills", "list"]) - rows = [line for line in listing.splitlines() if str(installed) in line] - names = sorted(line.split("|")[0].strip().removeprefix("agentic-development:") for line in rows) - if names != EXPECTED or any("/assets/" in line or "/references/" in line for line in rows): - raise ValueError("Unexpected Skills: " + json.dumps(names)) - for name in ("tests", "tools", "evidence", "integration", ".beads", "AGENTS.md"): - if (installed / name).exists(): - raise ValueError("Development leakage: " + name) - # Goose rewrites only the frontmatter name to its plugin-qualified form. - for entry in receipt["files"]: - data = (installed / entry["path"]).read_bytes() - if entry["path"].endswith("/SKILL.md"): - data = re.sub(rb"(?m)^name: agentic-development:", b"name: ", data, count=1) - if hashlib.sha256(data).hexdigest() != entry["sha256"]: - raise ValueError("Installed payload mismatch: " + entry["path"]) - meta = json.loads((installed / ".goose-plugin-install.json").read_text()) - if meta["source_type"] != "git" or not meta["auto_update"]: - raise ValueError("Git update metadata lost") - result = subprocess.run([sys.executable, "-I", str(installed / "scripts/observe.pyz")], - input=b'{"event":"SessionStart"}', capture_output=True, cwd=foreign, env=env, timeout=5) - if result.returncode or result.stdout: - raise ValueError("Installed observer runtime failed") - json.loads(result.stderr.decode().split(" ", 1)[1]) - - check() - legacy = installed / "skills/cycle/assets/evaluation-fixtures/demo" - legacy.mkdir(parents=True) - (legacy / "SKILL.md.fixture").write_text("obsolete fixture") - marker = repo / "README.md" - marker.write_bytes(marker.read_bytes() + b"\nIsolated update probe.\n") - # Update a payload file and its receipt, retaining the source-state distinction. - for entry in receipt["files"]: - if entry["path"] == "README.md": - entry["sha256"] = hashlib.sha256(marker.read_bytes()).hexdigest() - (repo / "distribution.json").write_bytes(production.encode(receipt)) - # The transport repository now has .git/; verify installed payload bytes - # against this updated receipt after replacement, not its Git metadata. - git("add", "README.md", "distribution.json") - git("commit", "-qm", "isolated update fixture") - run(executable, ["plugin", "update", "agentic-development"]) - if legacy.exists(): - raise ValueError("Update retained obsolete fixture") - check() - return {"status": "passed", "goose": version, "git_install": True, "git_update": True, - "skills_list_after_install": 7, "skills_list_after_update": 7, - "obsolete_fixture_removed": True, "installed_payload_hashes": "verified after both", - "runtime": "stdlib observer executed", "source_state": receipt["source_state"], - "release_approval": False} - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("candidate", type=Path) - parser.add_argument("--goose", default=os.environ.get("GOOSE_EXECUTABLE", "goose")) - args = parser.parse_args() - try: - print(json.dumps(probe(args.candidate.resolve(), args.goose), sort_keys=True)) - except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error: - parser.exit(1, "distribution-probe: " + str(error) + "\n") - - -if __name__ == "__main__": - main() diff --git a/tests/test_goose_distribution_probe.py b/tests/test_goose_distribution_probe.py deleted file mode 100644 index d45cab91..00000000 --- a/tests/test_goose_distribution_probe.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Public probe version-gate regressions; mocked host, not installation evidence.""" -import importlib.util -from pathlib import Path -import subprocess -from types import SimpleNamespace -import unittest -from unittest.mock import patch - -ROOT = Path(__file__).resolve().parents[1] -spec = importlib.util.spec_from_file_location("distribution_probe", ROOT / "tests/probe_goose_distribution.py") -probe_module = importlib.util.module_from_spec(spec) -spec.loader.exec_module(probe_module) - - -class DistributionProbeVersionTests(unittest.TestCase): - def run_until_install(self, version): - calls = [] - production = SimpleNamespace(verify=lambda _: {}, smoke=lambda _: None) - fake_spec = SimpleNamespace(loader=SimpleNamespace(exec_module=lambda _: None)) - - def run(command, **kwargs): - calls.append((command, kwargs)) - if command[1:] == ["--version"]: - return subprocess.CompletedProcess(command, 0, " " + version + "\n", "") - if command[1:3] == ["plugin", "install"]: - raise RuntimeError("installation boundary reached") - return subprocess.CompletedProcess(command, 0, "", "") - - with ( - patch.object(probe_module.importlib.util, "spec_from_file_location", return_value=fake_spec), - patch.object(probe_module.importlib.util, "module_from_spec", return_value=production), - patch.object(probe_module.shutil, "which", return_value="/verified/goose"), - patch.object(probe_module.shutil, "copytree"), - patch.object(probe_module.subprocess, "run", side_effect=run), - ): - try: - probe_module.probe(ROOT, "/verified/goose") - except (ValueError, RuntimeError) as error: - return error, calls - self.fail("Probe unexpectedly completed without installation") - - def test_selected_156_reaches_isolated_install(self): - error, calls = self.run_until_install("1.56.0") - self.assertEqual(str(error), "installation boundary reached") - command, kwargs = calls[-1] - self.assertEqual(command[:4], ["/verified/goose", "plugin", "install", "--auto-update"]) - self.assertTrue(command[4].startswith("file://")) - env = kwargs["env"] - home = Path(env["HOME"]) - self.assertEqual(Path(env["GOOSE_PATH_ROOT"]).parent, home.parent) - self.assertEqual(env["GIT_CONFIG_NOSYSTEM"], "1") - self.assertFalse(home.exists(), "Disposable root should be cleaned after failure") - - def test_unverified_versions_refused_before_git_or_install(self): - for version in ("1.47.0", "1.55.0", "1.56.1", "", "goose 1.56.0"): - with self.subTest(version=version): - error, calls = self.run_until_install(version) - self.assertIsInstance(error, ValueError) - self.assertIn("requires verified Goose 1.56.0", str(error)) - self.assertEqual(len(calls), 1) - - def test_documented_runtime_and_explicit_executable(self): - text = (ROOT / "CONTRIBUTING.md").read_text() - self.assertIn("The probe requires Goose 1.56.0", text) - self.assertIn("--goose /absolute/path/to/goose", text) - - -if __name__ == "__main__": - unittest.main()