diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44c2f0f..75c0626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - name: Check ORRO wrapper install run: python3 scripts/check_orro_wrapper_install.py --json - name: Check ORRO wrapper distribution - run: python3 scripts/check_orro_wrapper_distribution.py --json + run: python3 scripts/check_orro_wrapper_distribution.py --json --allow-network - name: Compile scripts run: python3 -m py_compile scripts/check_no_bidi_controls.py scripts/check_orro_repo_contract.py scripts/check_orro_language_boundaries.py scripts/check_orro_assurance_contract_fixtures.py scripts/check_orro_release_manifest.py scripts/check_compatibility_matrix.py scripts/check_orro_packaging_decision.py scripts/check_orro_fallback_policy.py scripts/check_orro_command_migration.py scripts/check_orro_command_migration_dry_run.py scripts/check_orro_wrapper.py scripts/check_orro_wrapper_install.py scripts/check_orro_wrapper_distribution.py scripts/orro_e2e_smoke.py scripts/update_orro_engine_lock.py scripts/bootstrap_orro.py src/orro_wrapper/__init__.py src/orro_wrapper/__main__.py src/orro_wrapper/cli.py - name: Bidi control scanner self-test diff --git a/README.md b/README.md index b09b732..7b6815d 100644 --- a/README.md +++ b/README.md @@ -180,11 +180,13 @@ temporary virtual environment, and verifies that `orro` and `orro-wrapper` are exposed: ```bash -python3 scripts/check_orro_wrapper_distribution.py --json +python3 scripts/check_orro_wrapper_distribution.py --json --allow-network ``` The distribution smoke checks that the wheel contains no Depone or witnessd packages and no proofrun/proofcheck runtime implementation files. +The explicit network flag authorizes pip build isolation to provision the +declared `setuptools>=61` build requirement in a clean build environment. The distribution smoke is local test metadata, not proof, not verifier truth, not package publish, not approval, and not assurance. diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 9d44b21..d600bf4 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -84,12 +84,13 @@ Published ORRO package remains future work. The wrapper distribution smoke is separate from bootstrap: ```bash -python3 scripts/check_orro_wrapper_distribution.py --json +python3 scripts/check_orro_wrapper_distribution.py --json --allow-network ``` It builds and installs a local wheel to verify packaging boundaries. It does not prepare engine checkouts, does not verify evidence, does not publish a package, -and does not shadow the witnessd-hosted `orro` command. +and does not shadow the witnessd-hosted `orro` command. The explicit network +flag authorizes only the isolated wrapper build-dependency bootstrap. ## Self-Test diff --git a/docs/engine-lock-update-process.md b/docs/engine-lock-update-process.md index f9a2fd1..95a1e4c 100644 --- a/docs/engine-lock-update-process.md +++ b/docs/engine-lock-update-process.md @@ -54,7 +54,7 @@ checkout or open an intentional engine-lock update PR. 3. Run `python3 scripts/check_orro_repo_contract.py`. 4. Run `python3 scripts/check_orro_release_manifest.py`. 5. Run `python3 scripts/update_orro_engine_lock.py --self-test`. -6. Run `python3 scripts/check_orro_wrapper_distribution.py --json`. +6. Run `python3 scripts/check_orro_wrapper_distribution.py --json --allow-network`. 7. Run `python3 scripts/orro_e2e_smoke.py --engine-lock engine-lock/orro-e2e-engine-lock.json --require-lock-match --allow-network`. 8. Confirm boundary text. 9. Open a PR with engine commits and test results. diff --git a/docs/install.md b/docs/install.md index cbc0d8f..52d47de 100644 --- a/docs/install.md +++ b/docs/install.md @@ -83,7 +83,9 @@ remains future work, and future wrapper work must contain no engine code. The local wrapper package can be checked without publishing anything: ```bash -python3 scripts/check_orro_wrapper_distribution.py --json +python3 scripts/check_orro_wrapper_distribution.py --json --allow-network ``` This builds and installs a local wheel and verifies `orro` plus `orro-wrapper`. +The flag authorizes pip build isolation to provision the declared wrapper build +dependency; it does not authorize engine checkout or package publication. diff --git a/docs/thin-wrapper.md b/docs/thin-wrapper.md index 4a672e3..e851860 100644 --- a/docs/thin-wrapper.md +++ b/docs/thin-wrapper.md @@ -91,12 +91,14 @@ not package publish, not approval, and not assurance. The wrapper distribution smoke builds and installs a local wheel: ```bash -python3 scripts/check_orro_wrapper_distribution.py --json +python3 scripts/check_orro_wrapper_distribution.py --json --allow-network ``` It verifies that the wheel contains wrapper modules only, exposes `orro` and `orro-wrapper`, and contains no Depone, witnessd, proofrun, proofcheck, scheduler, observer, fan-in, team-ledger, or verifier implementation files. +The explicit network flag authorizes only pip build isolation for the wrapper's +declared build dependency. The distribution smoke is local test metadata, not proof, not verifier truth, not package publish, not approval, and not assurance. diff --git a/docs/wrapper-distribution.md b/docs/wrapper-distribution.md index a0e9ea8..0eca9b1 100644 --- a/docs/wrapper-distribution.md +++ b/docs/wrapper-distribution.md @@ -9,12 +9,16 @@ truth, not package publish, not approval, and not assurance. ## Command ```bash -python3 scripts/check_orro_wrapper_distribution.py --json +python3 scripts/check_orro_wrapper_distribution.py --json --allow-network ``` The checker builds a local wheel in a temporary workspace, installs that wheel into a temporary virtual environment, and verifies the installed `orro` and -`orro-wrapper` console scripts. +`orro-wrapper` console scripts. It creates a separate build environment, removes +any preinstalled `setuptools`, confirms the build backend is absent, and then +uses pip build isolation to provision the declared `setuptools>=61` requirement. +`--allow-network` explicitly authorizes that build-dependency bootstrap; it does +not authorize engine checkout mutation or package publication. ## Boundary diff --git a/scripts/check_orro_wrapper_distribution.py b/scripts/check_orro_wrapper_distribution.py index 22a8ebc..b4d1ba3 100644 --- a/scripts/check_orro_wrapper_distribution.py +++ b/scripts/check_orro_wrapper_distribution.py @@ -182,15 +182,36 @@ def check_boundary_payload(label: str, payload: dict[str, Any]) -> None: fail("ERR_ORRO_WRAPPER_DISTRIBUTION_BOUNDARY_INVALID", f"{label}.boundary.{key} must be false") -def build_wheel(source_dir: Path, dist_dir: Path) -> Path: - run_command([sys.executable, "-m", "pip", "wheel", "--no-deps", "--no-build-isolation", "-w", str(dist_dir), str(source_dir)]) +def prepare_build_venv(venv_dir: Path) -> Path: + create_venv(venv_dir) + bin_dir = scripts_dir(venv_dir) + python = bin_dir / ("python.exe" if os.name == "nt" else "python") + run_command([str(python), "-m", "pip", "uninstall", "--yes", "setuptools"]) + probe = run_command( + [ + str(python), + "-c", + "import importlib.util; print(importlib.util.find_spec('setuptools') is not None)", + ] + ) + if probe.stdout.strip() != "False": + fail( + "ERR_ORRO_WRAPPER_DISTRIBUTION_BUILD_ENV_INVALID", + "wrapper build venv must not contain setuptools before the isolated build", + {"stdout": probe.stdout, "stderr": probe.stderr}, + ) + return python + + +def build_wheel(source_dir: Path, dist_dir: Path, python: Path) -> Path: + run_command([str(python), "-m", "pip", "wheel", "--no-deps", "-w", str(dist_dir), str(source_dir)]) wheels = sorted(dist_dir.glob("orro_product_wrapper-*.whl")) if len(wheels) != 1: fail("ERR_ORRO_WRAPPER_DISTRIBUTION_WHEEL_NOT_FOUND", "expected exactly one ORRO wrapper wheel", {"wheels": [str(path) for path in wheels]}) return wheels[0] -def distribution_check(workspace: Path | None) -> dict[str, Any]: +def distribution_check(workspace: Path | None, *, allow_network: bool) -> dict[str, Any]: owns_workspace = workspace is None workdir = Path(tempfile.mkdtemp(prefix="orro-wrapper-dist-")) if workspace is None else workspace if workdir.exists() and any(workdir.iterdir()): @@ -198,11 +219,19 @@ def distribution_check(workspace: Path | None) -> dict[str, Any]: workdir.mkdir(parents=True, exist_ok=True) source_dir = workdir / "source" dist_dir = workdir / "dist" + build_venv_dir = workdir / "build-venv" venv_dir = workdir / "venv" try: copy_source(source_dir) dist_dir.mkdir() - wheel_path = build_wheel(source_dir, dist_dir) + build_python = prepare_build_venv(build_venv_dir) + if not allow_network: + fail( + "ERR_ORRO_WRAPPER_DISTRIBUTION_NETWORK_NOT_ALLOWED", + "isolated wrapper build dependency bootstrap requires --allow-network", + {"build_backend": "setuptools.build_meta", "build_requirement": "setuptools>=61"}, + ) + wheel_path = build_wheel(source_dir, dist_dir, build_python) package_contents = inspect_wheel(wheel_path) entry_points = inspect_entry_points(wheel_path) @@ -239,6 +268,7 @@ def distribution_check(workspace: Path | None) -> dict[str, Any]: "wheel_entry_points": entry_points, "package_contents": package_contents, "checks": [ + {"name": "build_venv_has_no_setuptools_before_build", "status": "pass"}, {"name": "wheel_build", "status": "pass"}, {"name": "wheel_contains_no_engine_packages", "status": "pass"}, {"name": "wheel_entry_points_include_orro", "status": "pass"}, @@ -303,6 +333,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Build and smoke-test the local ORRO wrapper wheel.") parser.add_argument("--workspace", "--workdir", dest="workspace", help="Empty workspace for build/install artifacts. Defaults to a temporary directory.") parser.add_argument("--json", action="store_true", help="Emit JSON. JSON is the default output.") + parser.add_argument( + "--allow-network", + action="store_true", + help="Allow pip build isolation to bootstrap the wrapper's declared build dependency.", + ) parser.add_argument("--self-test", action="store_true", help="Run offline self-test without building a wheel.") return parser.parse_args(argv) @@ -310,7 +345,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(sys.argv[1:] if argv is None else argv) try: - result = self_test() if args.self_test else distribution_check(Path(args.workspace) if args.workspace else None) + result = ( + self_test() + if args.self_test + else distribution_check(Path(args.workspace) if args.workspace else None, allow_network=args.allow_network) + ) print(json.dumps(result, indent=2, sort_keys=True)) return 0 except DistributionCheckError as exc: diff --git a/scripts/orro_e2e_smoke.py b/scripts/orro_e2e_smoke.py index b9aa137..3818239 100644 --- a/scripts/orro_e2e_smoke.py +++ b/scripts/orro_e2e_smoke.py @@ -25,6 +25,7 @@ SCHEMA_VERSION = "0.1" COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") LOCK_BOUNDARY_FALSE_KEYS = ("approves_merge", "raises_assurance", "executes_commands", "verifies_evidence") +ROLE_LANE_PLACEHOLDER_PROMPT_PREFIX = "Execute ORRO role " class OrroE2EError(RuntimeError): @@ -112,6 +113,25 @@ def _self_test() -> int: lambda: _engine_lock_status(mismatch, True, witnessd_repo, depone_repo), checks, ) + role_lane_plan = { + "lanes": [ + { + "lane_id": "runner", + "may_execute": True, + "prompt": "Execute ORRO role runner for goal: update README safely", + }, + { + "lane_id": "reviewer", + "may_execute": False, + "prompt": "Review the README without changing it", + }, + ] + } + materialized = _materialize_executable_role_lane_prompts(role_lane_plan, "update README safely") + assert materialized == 1 + assert role_lane_plan["lanes"][0]["prompt"] == "update README safely" + assert role_lane_plan["lanes"][1]["prompt"] == "Review the README without changing it" + checks.append({"name": "executable_role_lane_prompt_materialization", "status": "pass"}) result = _json_result("pass", checks) assert result["kind"] == "orro-e2e-smoke-result" assert result["boundary"]["contains_engine_logic"] is False @@ -309,6 +329,26 @@ def _run_raw(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]: return completed +def _materialize_executable_role_lane_prompts(role_lane_plan: dict[str, Any], task: str) -> int: + lanes = role_lane_plan.get("lanes") + if not isinstance(lanes, list): + raise OrroE2EError( + "ERR_ORRO_E2E_ROLE_LANE_PLAN_INVALID", + "role-lane plan lanes must be a list", + ) + materialized = 0 + for lane in lanes: + if not isinstance(lane, dict): + raise OrroE2EError( + "ERR_ORRO_E2E_ROLE_LANE_PLAN_INVALID", + "role-lane plan lanes must contain objects", + ) + if lane.get("may_execute") is True: + lane["prompt"] = task + materialized += 1 + return materialized + + class SmokeRunner: def __init__( self, @@ -532,6 +572,35 @@ def _full_flow(self) -> None: self._assert(workflow_plan.is_file(), "flowplan_writes_workflow_plan", "workflow plan missing") self._assert(role_lane_plan.is_file(), "flowplan_writes_role_lane_plan", "role-lane plan missing") + try: + role_lane_payload = json.loads(role_lane_plan.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise OrroE2EError( + "ERR_ORRO_E2E_ROLE_LANE_PLAN_INVALID", + "could not load generated role-lane plan", + {"path": str(role_lane_plan), "error": str(exc)}, + ) from exc + if not isinstance(role_lane_payload, dict): + raise OrroE2EError( + "ERR_ORRO_E2E_ROLE_LANE_PLAN_INVALID", + "generated role-lane plan must be a JSON object", + {"path": str(role_lane_plan)}, + ) + materialized_lanes = _materialize_executable_role_lane_prompts(role_lane_payload, goal) + role_lane_plan.write_text(json.dumps(role_lane_payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + executable_lanes = [lane for lane in role_lane_payload["lanes"] if lane.get("may_execute") is True] + self._assert( + materialized_lanes > 0 + and all( + lane.get("prompt") == goal + and not str(lane.get("prompt", "")).startswith(ROLE_LANE_PLACEHOLDER_PROMPT_PREFIX) + for lane in executable_lanes + ), + "flowplan_materializes_explicit_role_lane_prompts", + "every executable role lane must carry the declared E2E task before proofrun", + materialized_lanes=materialized_lanes, + ) + proofrun_args = [ "proofrun", goal,