From 6e227f62a35d55e46a0b5d684d839c069958cc37 Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 13 Aug 2026 11:24:31 +0200 Subject: [PATCH] fix(codex): select a compatible Python runtime --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 9 +++ CITATION.cff | 2 +- README.md | 5 +- codemeta.json | 2 +- docs/integrations/codex.md | 4 ++ docs/operations/codex-plugin-submission.md | 16 +++-- docs/operations/codex-plugin-test-cases.json | 2 +- .../codex-plugin-smoke-2026-08-13-v0.3.3.json | 17 +++++ plugins/marginal/.codex-plugin/plugin.json | 2 +- plugins/marginal/runtime/marginal_runtime.pyz | Bin 399633 -> 399633 bytes plugins/marginal/runtime/provenance.json | 2 +- plugins/marginal/scripts/marginal_control.py | 11 ++- plugins/marginal/scripts/marginal_hook.py | 9 ++- plugins/marginal/scripts/runtime_python.py | 63 ++++++++++++++++++ plugins/marginal/skills/marginal/SKILL.md | 2 + pyproject.toml | 2 +- scripts/smoke_codex_plugin.py | 9 ++- src/marginal/__init__.py | 2 +- src/marginal/integrations/codex/identity.py | 2 +- .../codex/test_marketplace_smoke.py | 1 + tests/plugin/test_codex_plugin.py | 23 +++++++ tests/test_public_api_v2.py | 2 +- tests/test_repository_consistency_v2.py | 2 +- 24 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.3.json create mode 100644 plugins/marginal/scripts/runtime_python.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6ea90da..33dbf32 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: MARGINAL version - placeholder: "0.3.2" + placeholder: "0.3.3" validations: required: true - type: input diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a22d2c..8db83a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to MARGINAL are documented here. The project follows Semanti ## [Unreleased] +## [0.3.3] - 2026-08-13 + +### Fixed + +- native hook and control launchers now replace an incompatible `python3` (including macOS Xcode + Python 3.9) with an available Python 3.10–3.13 interpreter; +- marketplace smoke now invokes the exact public `python3` launcher path and records its version, + preventing isolated test environments from masking interpreter-selection failures. + ## [0.3.2] - 2026-08-13 ### Added diff --git a/CITATION.cff b/CITATION.cff index 143fda5..b615667 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,7 +4,7 @@ title: "MARGINAL: Economically Disciplined Compute Allocation for AI Agents" type: software authors: - name: SignalLayer Labs -version: 0.3.2 +version: 0.3.3 date-released: 2026-08-13 license: Apache-2.0 repository-code: "https://github.com/SignalLayerLabs/Marginal" diff --git a/README.md b/README.md index f60de20..33238d8 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,8 @@ codex plugin marketplace add SignalLayerLabs/Marginal --ref main && codex plugin Then open `/hooks` in Codex, review the exact commands, and grant trust only after inspection. MARGINAL never bypasses the hook trust boundary. No Python package or global executable is needed. -In Codex, ask the bundled skill directly: +One local Python 3.10–3.13 interpreter is required; the launcher finds it automatically even when +`python3` points to an older macOS/Xcode runtime. In Codex, ask the bundled skill directly: ```text Use $marginal to report whether a live hook service is active, whether hooks were observed, and whether this repository is in Shadow Mode or Tool Enforcement. @@ -237,7 +238,7 @@ marginal install codex Current tagged library install target: ```bash -pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.3.2" +pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.3.3" ``` Development checkout: diff --git a/codemeta.json b/codemeta.json index 796b7f9..74a0073 100644 --- a/codemeta.json +++ b/codemeta.json @@ -6,7 +6,7 @@ "codeRepository": "https://github.com/SignalLayerLabs/Marginal", "issueTracker": "https://github.com/SignalLayerLabs/Marginal/issues", "license": "https://spdx.org/licenses/Apache-2.0", - "version": "0.3.2", + "version": "0.3.3", "datePublished": "2026-08-06", "programmingLanguage": "Python", "runtimePlatform": "Python 3.10-3.13", diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md index 7f57b5a..110f98c 100644 --- a/docs/integrations/codex.md +++ b/docs/integrations/codex.md @@ -14,6 +14,10 @@ Open `/hooks` in Codex and inspect the four MARGINAL lifecycle commands before g plugin never uses the bypass-trust flag. Until trust and runtime coverage are observed, MARGINAL is unobserved or Shadow-only; lack of evidence alone does not prove that hooks are disabled. +The bundle has no third-party Python dependency, but requires one local Python 3.10–3.13 runtime. +Its launchers automatically select a compatible version even when `python3` resolves to macOS/Xcode +Python 3.9. If none is available, control commands return the exact requirement and hooks fail open. + An installed Python package can perform the same native transaction: ```bash diff --git a/docs/operations/codex-plugin-submission.md b/docs/operations/codex-plugin-submission.md index 7b998b4..7c50ab8 100644 --- a/docs/operations/codex-plugin-submission.md +++ b/docs/operations/codex-plugin-submission.md @@ -3,7 +3,7 @@ ```text status: not_submitted status_date: 2026-08-13 -plugin_version: 0.3.2 +plugin_version: 0.3.3 marketplace_selector: marginal@marginal portal_submission_type: skills_only_zip portal_access: authentication_required @@ -32,9 +32,9 @@ and permits repository Tool Enforcement only after a versioned evidence gate pro reviewed false stops, and governance overhead. It never reads prompts, source, raw commands, raw outputs, transcripts, or Codex credentials for evidence. -**Release notes:** Adds a dependency-free native control plane, repository-scoped hook attestation, -local Shadow Mode, review workflow, Earned Enforcement gates, fail-open demotion, production logo, -eight reviewer cases, and a reproducible v0.3.2 archive. +**Release notes:** Adds automatic compatible-Python discovery, a dependency-free native control +plane, repository-scoped hook attestation, local Shadow Mode, review workflow, Earned Enforcement +gates, fail-open demotion, production logo, eight reviewer cases, and a reproducible v0.3.3 archive. Starter prompts are the three `interface.defaultPrompt` entries in `plugins/marginal/.codex-plugin/plugin.json`. The production logo is @@ -63,14 +63,16 @@ coverage or identity changes. - [x] Official plugin validator passes. - [x] Skill validator passes. - [x] Isolated Codex 0.147.0 marketplace add/install/remove smoke passes. +- [x] Public `python3` launcher smoke passes from macOS/Xcode Python 3.9.6 by selecting a compatible + bundled-runtime interpreter. - [x] Four-event direct lifecycle smoke passes with 100% exercised coverage. - [x] Secret-marker scan returns zero persisted occurrences. - [x] Reproducible smoke evidence is committed with runtime SHA-256 provenance. - [x] Privacy, terms, support, and eight reviewer cases exist. -- [ ] Canonical main contains v0.3.2 after merge and every public Pages URL resolves. +- [ ] Canonical main contains v0.3.3 after merge and every public Pages URL resolves. - [x] A deterministic single-root ZIP builder covers the manifest, skill, hooks, runtime, and logo. - [ ] SignalLayer Labs identity and Apps Management write permission are confirmed in Platform. -- [ ] The v0.3.2 archive is uploaded in Platform and external review is started. +- [ ] The v0.3.3 archive is uploaded in Platform and external review is started. Build the exact upload artifact with: @@ -89,4 +91,4 @@ verified SignalLayer Labs identity; none can be inferred from repository or GitH external identifier, credential, or reviewer correspondence belongs in this repository. Acceptance evidence: -[`codex-plugin-smoke-2026-08-13-v0.3.2.json`](evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json). +[`codex-plugin-smoke-2026-08-13-v0.3.3.json`](evidence/codex-plugin-smoke-2026-08-13-v0.3.3.json). diff --git a/docs/operations/codex-plugin-test-cases.json b/docs/operations/codex-plugin-test-cases.json index 5f84509..e719b54 100644 --- a/docs/operations/codex-plugin-test-cases.json +++ b/docs/operations/codex-plugin-test-cases.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "plugin_version": "0.3.2", + "plugin_version": "0.3.3", "positive": [ { "id": "positive-shadow-status", diff --git a/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.3.json b/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.3.json new file mode 100644 index 0000000..0bb8181 --- /dev/null +++ b/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.3.json @@ -0,0 +1,17 @@ +{ + "codex_version": "codex-cli 0.147.0", + "completed_sessions": 1, + "directory_archive_sha256": "42edc01ce21753ce9676426f6d89b0a249539545df0d3c2073c1c060e1b39f7b", + "evidence_records": 4, + "hook_coverage": 1.0, + "installed": true, + "launcher_python_version": "Python 3.9.6", + "marketplace_selector": "marginal@marginal", + "native_control_mode": "shadow", + "native_control_observed": true, + "plugin_version": "0.3.3", + "raw_secret_occurrences": 0, + "removed": true, + "runtime_sha256": "9843b084dd481f29762870e6e6f79a301bdd9c15b7a8c2dc1c4104abc332b259", + "shadow_block_count": 0 +} diff --git a/plugins/marginal/.codex-plugin/plugin.json b/plugins/marginal/.codex-plugin/plugin.json index d1c601a..f0bfda0 100644 --- a/plugins/marginal/.codex-plugin/plugin.json +++ b/plugins/marginal/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "marginal", - "version": "0.3.2", + "version": "0.3.3", "description": "Evidence-gated compute governance for Codex, local-first and Shadow Mode by default.", "author": { "name": "SignalLayer Labs", diff --git a/plugins/marginal/runtime/marginal_runtime.pyz b/plugins/marginal/runtime/marginal_runtime.pyz index 0dafd6c1df9d3c6d9773862f3e719c353539c8a2..c980695c765904140f02744663655d4542821de2 100644 GIT binary patch delta 87 zcmbQZNn+wAi48K0EN7Dh_HI^SEaYP}Zl1)yeG)&TPyvh9qqRKSSJX3RGBO&s3pFwV hF%u9o12M~Xp+?qQ9bkRiKX9_$VS%W=z{lpu1ppPx9k>7h delta 87 zcmbQZNn+wAi48K0EWbD|?b)orSjfj{)I5oQ`y_rwp#m1KhMAMMuc&9tWMnjI7iwe# hVkRJF24a@&LXE7qI>7q2f8b=h!vax#fsf6R3jkI@A3y*A diff --git a/plugins/marginal/runtime/provenance.json b/plugins/marginal/runtime/provenance.json index 464af46..7052df1 100644 --- a/plugins/marginal/runtime/provenance.json +++ b/plugins/marginal/runtime/provenance.json @@ -1 +1 @@ -{"builder":"scripts/build_codex_plugin.py","python_requires":">=3.10","schema_version":1,"sha256":"7ea67b6a17d8f44c03b70e29ec29497a00edd0f9c4262e59cb9082f2015eafe6","source_hash":"ab9dfa18a1ddf72f65a64cc7aba86ca0bd1b4e79e0747c983078e5b8ee87e042"} +{"builder":"scripts/build_codex_plugin.py","python_requires":">=3.10","schema_version":1,"sha256":"9843b084dd481f29762870e6e6f79a301bdd9c15b7a8c2dc1c4104abc332b259","source_hash":"081703fe680da21b5d261dc1828071f8667c73ed74afe58399b33a25cf98360e"} diff --git a/plugins/marginal/scripts/marginal_control.py b/plugins/marginal/scripts/marginal_control.py index 813442d..ea49cfc 100644 --- a/plugins/marginal/scripts/marginal_control.py +++ b/plugins/marginal/scripts/marginal_control.py @@ -7,6 +7,8 @@ import sys from pathlib import Path +from runtime_python import compatible_python + def _plugin_data() -> Path: configured = os.environ.get("PLUGIN_DATA") @@ -37,6 +39,11 @@ def main(argv: list[str] | None = None) -> int: if not runtime.is_file(): print(f"MARGINAL runtime not found: {runtime}", file=sys.stderr) return 1 + try: + python = compatible_python() + except RuntimeError as exc: + print(str(exc), file=sys.stderr) + return 1 data = _plugin_data() data.mkdir(parents=True, exist_ok=True, mode=0o700) @@ -48,9 +55,9 @@ def main(argv: list[str] | None = None) -> int: environment["PLUGIN_DATA"] = str(data) environment["PLUGIN_ROOT"] = str(plugin_root) os.execve( - sys.executable, + python[0], [ - sys.executable, + *python, str(runtime), "codex", arguments[0], diff --git a/plugins/marginal/scripts/marginal_hook.py b/plugins/marginal/scripts/marginal_hook.py index a08a623..34a21df 100644 --- a/plugins/marginal/scripts/marginal_hook.py +++ b/plugins/marginal/scripts/marginal_hook.py @@ -4,9 +4,10 @@ from __future__ import annotations import os -import sys from pathlib import Path +from runtime_python import compatible_python + def main() -> int: plugin_root = os.environ.get("PLUGIN_ROOT") @@ -16,6 +17,10 @@ def main() -> int: runtime = Path(plugin_root).resolve() / "runtime" / "marginal_runtime.pyz" if not runtime.is_file(): return 0 + try: + python = compatible_python() + except RuntimeError: + return 0 environment = { name: value for name in ("PATH", "LANG", "LC_ALL", "SYSTEMROOT") @@ -23,7 +28,7 @@ def main() -> int: } environment["PLUGIN_DATA"] = str(Path(plugin_data).resolve()) environment["PLUGIN_ROOT"] = str(Path(plugin_root).resolve()) - os.execve(sys.executable, [sys.executable, str(runtime)], environment) + os.execve(python[0], [*python, str(runtime)], environment) return 0 diff --git a/plugins/marginal/scripts/runtime_python.py b/plugins/marginal/scripts/runtime_python.py new file mode 100644 index 0000000..de52a3c --- /dev/null +++ b/plugins/marginal/scripts/runtime_python.py @@ -0,0 +1,63 @@ +"""Select a Python interpreter compatible with the bundled MARGINAL runtime.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys + +_MINIMUM_VERSION = (3, 10) +_VERSIONED_NAMES = ("python3.13", "python3.12", "python3.11", "python3.10") + + +def _probe(command: tuple[str, ...]) -> bool: + environment = { + name: value + for name in ("PATH", "SYSTEMROOT") + if (value := os.environ.get(name)) is not None + } + environment["PYTHONNOUSERSITE"] = "1" + try: + completed = subprocess.run( + [ + *command, + "-I", + "-c", + "import sys; raise SystemExit(sys.version_info < (3, 10))", + ], + check=False, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=environment, + timeout=2, + ) + except (OSError, subprocess.TimeoutExpired): + return False + return completed.returncode == 0 + + +def compatible_python() -> tuple[str, ...]: + """Return an executable command for Python 3.10+ without importing user packages.""" + + if sys.version_info[:2] >= _MINIMUM_VERSION: + return (sys.executable,) + + for name in _VERSIONED_NAMES: + executable = shutil.which(name) + if executable: + return (executable,) + + if os.name == "nt": + launcher = shutil.which("py") + if launcher: + for version in ("-3.13", "-3.12", "-3.11", "-3.10"): + command = (launcher, version) + if _probe(command): + return command + + python3 = shutil.which("python3") + if python3 and _probe((python3,)): + return (python3,) + raise RuntimeError("MARGINAL requires Python 3.10 or newer") diff --git a/plugins/marginal/skills/marginal/SKILL.md b/plugins/marginal/skills/marginal/SKILL.md index 239d996..f6953c2 100644 --- a/plugins/marginal/skills/marginal/SKILL.md +++ b/plugins/marginal/skills/marginal/SKILL.md @@ -20,6 +20,8 @@ Do not require a global `marginal` executable or a pip installation. Resolve the Pass `--workspace ` and `--json` when inspecting repository-scoped state. The launcher uses Codex's native plugin data directory, so hook evidence and control commands share one state. +It automatically replaces an older `python3` with an available Python 3.10–3.13 interpreter. If +none is installed, report that exact runtime requirement and do not claim hooks are operational. ## Workflow diff --git a/pyproject.toml b/pyproject.toml index 5c07dcb..17e9257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "marginal-ai" -version = "0.3.2" +version = "0.3.3" description = "Universal learning-loop and compute-governance foundation for economically disciplined AI agents." readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/smoke_codex_plugin.py b/scripts/smoke_codex_plugin.py index 0e213d9..781d5f0 100644 --- a/scripts/smoke_codex_plugin.py +++ b/scripts/smoke_codex_plugin.py @@ -7,7 +7,6 @@ import json import os import subprocess -import sys import time from dataclasses import asdict, dataclass from pathlib import Path @@ -23,6 +22,7 @@ class CodexPluginSmokeResult: completed_sessions: int native_control_observed: bool native_control_mode: str + launcher_python_version: str raw_secret_occurrences: int removed: bool codex_version: str @@ -131,6 +131,8 @@ def smoke_plugin( } _initialize_repository(workspace, environment) version = _run([str(codex), "--version"], environment=environment, cwd=root).stdout.strip() + launcher = _run(["python3", "--version"], environment=environment, cwd=root) + launcher_python_version = (launcher.stdout or launcher.stderr).strip() _run( [str(codex), "plugin", "marketplace", "add", str(marketplace), "--json"], environment=environment, @@ -161,7 +163,7 @@ def smoke_plugin( try: for payload in _hook_payloads(workspace, secret): result = _run( - [sys.executable, str(hook_script)], + ["python3", str(hook_script)], environment=hook_environment, cwd=workspace, input_text=json.dumps(payload), @@ -177,7 +179,7 @@ def smoke_plugin( time.sleep(0.02) control = _run( [ - sys.executable, + "python3", str(plugin_root / "scripts" / "marginal_control.py"), "status", "--workspace", @@ -219,6 +221,7 @@ def smoke_plugin( ), native_control_observed=native_control_observed, native_control_mode=native_control_mode, + launcher_python_version=launcher_python_version, raw_secret_occurrences=_count_secret(plugin_data, secret), removed=removed, codex_version=version, diff --git a/src/marginal/__init__.py b/src/marginal/__init__.py index 8234636..037a4e3 100644 --- a/src/marginal/__init__.py +++ b/src/marginal/__init__.py @@ -137,4 +137,4 @@ "validate_safe_telemetry_record", ] -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/src/marginal/integrations/codex/identity.py b/src/marginal/integrations/codex/identity.py index 488f21b..fdda22c 100644 --- a/src/marginal/integrations/codex/identity.py +++ b/src/marginal/integrations/codex/identity.py @@ -10,7 +10,7 @@ from .installer import CommandRunner, SubprocessRunner, inspect_codex from .promotion import PromotionIdentity -PLUGIN_VERSION = "0.3.2" +PLUGIN_VERSION = "0.3.3" ADAPTER_VERSION = "1" POLICY_HASH = hashlib.sha256(b"marginal:no-progress:v1:max-same-evidence=2").hexdigest() DEFAULT_HOOK_HASH = "46b7a85a3a542957d055c615ab501f9fee284bb3193ac9ecfbe8951cce5a9942" diff --git a/tests/integrations/codex/test_marketplace_smoke.py b/tests/integrations/codex/test_marketplace_smoke.py index 66524db..c38aacb 100644 --- a/tests/integrations/codex/test_marketplace_smoke.py +++ b/tests/integrations/codex/test_marketplace_smoke.py @@ -24,5 +24,6 @@ def test_marketplace_install_and_remove(tmp_path: Path) -> None: assert result.completed_sessions == 1 assert result.native_control_observed is True assert result.native_control_mode == "shadow" + assert result.launcher_python_version.startswith("Python 3.") assert result.raw_secret_occurrences == 0 assert result.removed is True diff --git a/tests/plugin/test_codex_plugin.py b/tests/plugin/test_codex_plugin.py index 1c92fbb..35c965b 100644 --- a/tests/plugin/test_codex_plugin.py +++ b/tests/plugin/test_codex_plugin.py @@ -1,12 +1,14 @@ from __future__ import annotations import hashlib +import importlib.util import json import os import subprocess import sys import zipfile from pathlib import Path +from types import SimpleNamespace import pytest from scripts.build_codex_plugin import build_plugin_runtime @@ -245,6 +247,26 @@ def test_native_control_preserves_codex_home_for_public_cli_discovery(tmp_path: assert payload["plugins_enabled"] is True +def test_runtime_selector_replaces_an_incompatible_launcher_python(monkeypatch) -> None: + module_path = PLUGIN / "scripts" / "runtime_python.py" + spec = importlib.util.spec_from_file_location("marginal_plugin_runtime_python", module_path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + monkeypatch.setattr( + module, + "sys", + SimpleNamespace(version_info=(3, 9, 6), executable="/usr/bin/python3"), + ) + monkeypatch.setattr( + module.shutil, + "which", + lambda name: "/opt/runtime/python3.11" if name == "python3.11" else None, + ) + + assert module.compatible_python() == ("/opt/runtime/python3.11",) + + def test_plugin_runtime_contains_no_live_repository_paths() -> None: runtime = (PLUGIN / "runtime" / "marginal_runtime.pyz").read_bytes() @@ -265,6 +287,7 @@ def test_skill_teaches_truthful_earned_enforcement_workflow() -> None: "scripts/marginal_control.py", "hooks_active", "hooks_observed", + "Python 3.10", "never claim token savings", ): assert phrase.casefold() in text.casefold() diff --git a/tests/test_public_api_v2.py b/tests/test_public_api_v2.py index 0f052f8..c170c17 100644 --- a/tests/test_public_api_v2.py +++ b/tests/test_public_api_v2.py @@ -23,7 +23,7 @@ def test_v02_public_exports_and_version() -> None: "ValueEstimate", } assert expected.issubset(set(marginal.__all__)) - assert marginal.__version__ == "0.3.2" + assert marginal.__version__ == "0.3.3" def test_json_schemas_exist_and_are_valid() -> None: diff --git a/tests/test_repository_consistency_v2.py b/tests/test_repository_consistency_v2.py index 840a7e2..3fbcf7f 100644 --- a/tests/test_repository_consistency_v2.py +++ b/tests/test_repository_consistency_v2.py @@ -20,7 +20,7 @@ def test_public_repository_identity_is_consistent() -> None: assert "SignalLayer Labs" in text or "SignalLayerLabs" in text, path codemeta = json.loads((ROOT / "codemeta.json").read_text(encoding="utf-8")) - assert codemeta["version"] == "0.3.2" + assert codemeta["version"] == "0.3.3" assert codemeta["codeRepository"] == "https://github.com/SignalLayerLabs/Marginal" assert codemeta["issueTracker"] == "https://github.com/SignalLayerLabs/Marginal/issues" assert codemeta["author"]["name"] == "SignalLayer Labs"