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
16 changes: 16 additions & 0 deletions docs/CODEX_0152_QUALIFICATION_TRUST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Codex 0.152 qualification trust contract

Live PlanAnvil qualification must model Codex CLI 0.152 project trust using the supported user-config path.

For C06, C08, C09, and C13 qualification probes:

- use a disposable, isolated `CODEX_HOME`;
- bridge authentication read-only through the existing qualification helper;
- persist `[projects."<absolute-project-path>"] trust_level = "trusted"` in that isolated home's `config.toml`;
- do not pass `projects.<path>.trust_level` through `-c/--config`;
- do not use `--ignore-user-config`, because that would discard the persisted trust decision;
- clean the isolated home after the probe and verify authentication metadata was unchanged.

This is qualification infrastructure, not a product configuration requirement. The PlanAnvil repository must not persist machine-specific trust paths.

C13 remains ephemeral-first. A recognized Codex 0.152 parent-thread registration failure (`collab spawn failed: no thread with id`) is a transport blocker even if `SubagentStart` fired before the failure. Only that recognized blocker permits the controlled non-ephemeral isolated-home fallback.
129 changes: 129 additions & 0 deletions tests/test_codex0152_persisted_trust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from __future__ import annotations

import os
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock


ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))

import live_codex_qualification_codex0152 as compat


class Codex0152PersistedTrustTests(unittest.TestCase):
def test_persisted_trust_file_is_idempotent_and_enables_hooks(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
home = root / "home"
repo = root / "repo"
repo.mkdir()

compat._write_persisted_project_trust(home, repo)
compat._write_persisted_project_trust(home, repo)

text = (home / "config.toml").read_text(encoding="utf-8")
header = f"[projects.{compat.base.toml_quote(str(repo.resolve()))}]"
self.assertIn("[features]", text)
self.assertIn("hooks = true", text)
self.assertEqual(text.count(header), 1)
self.assertIn('trust_level = "trusted"', text)

def test_persisted_trust_args_remove_invalid_cli_trust_and_load_user_config(self) -> None:
original = compat.base.common_codex_args
calls: list[dict[str, object]] = []

def fake_common(**kwargs: object) -> list[str]:
calls.append(dict(kwargs))
args = ["codex", "exec", "--ignore-user-config"]
if kwargs.get("trust_project"):
args += ["-c", 'projects."/tmp/repo".trust_level="trusted"']
return args

compat.base.common_codex_args = fake_common
try:
with compat._persisted_trust_args():
args = compat.base.common_codex_args(
cwd=Path("/tmp/repo"),
sandbox="read-only",
schema=Path("schema.json"),
output=Path("output.json"),
trust_project=True,
)
finally:
compat.base.common_codex_args = original

self.assertFalse(bool(calls[-1]["trust_project"]))
self.assertNotIn("--ignore-user-config", args)
self.assertFalse(any("projects." in item for item in args))

def test_isolated_runtime_persists_each_probed_cwd_and_restores_environment(self) -> None:
original_common = compat.base.common_codex_args
previous_home = os.environ.get("CODEX_HOME")

def fake_common(**kwargs: object) -> list[str]:
args = ["codex", "exec", "--ignore-user-config"]
if kwargs.get("trust_project"):
args += ["-c", 'projects."bad".trust_level="trusted"']
return args

with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
cap_runtime = root / "runtime"
home = root / "isolated-home"
repo_a = root / "repo-a"
repo_b = root / "repo-b"
home.mkdir()
repo_a.mkdir()
repo_b.mkdir()

compat.base.common_codex_args = fake_common
try:
with (
mock.patch.object(
compat.v5,
"_prepare_isolated_codex_home",
return_value=(home, None, None),
),
mock.patch.object(
compat.v5,
"_cleanup_isolated_codex_home",
return_value=(True, True),
) as cleanup,
compat._isolated_persisted_trust_runtime(cap_runtime),
):
self.assertEqual(os.environ.get("CODEX_HOME"), str(home))
args_a = compat.base.common_codex_args(
cwd=repo_a,
sandbox="read-only",
schema=Path("schema.json"),
output=Path("output.json"),
trust_project=True,
)
args_b = compat.base.common_codex_args(
cwd=repo_b,
sandbox="read-only",
schema=Path("schema.json"),
output=Path("output.json"),
trust_project=True,
)

cleanup.assert_called_once_with(home, None, None)
finally:
compat.base.common_codex_args = original_common

text = (home / "config.toml").read_text(encoding="utf-8")
self.assertIn(str(repo_a.resolve()), text)
self.assertIn(str(repo_b.resolve()), text)
self.assertNotIn("--ignore-user-config", args_a)
self.assertNotIn("--ignore-user-config", args_b)
self.assertFalse(any("projects." in item for item in args_a + args_b))

self.assertEqual(os.environ.get("CODEX_HOME"), previous_home)


if __name__ == "__main__":
unittest.main()
10 changes: 10 additions & 0 deletions tests/test_codex0152_product_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ def test_product_requires_exact_subagent_roles(self) -> None:
self.assertIn("`SubagentStart` matcher input is the spawned `agent_type`", self.contract)
self.assertIn("`plan_anvil_profiler`", self.contract)
self.assertIn("`plan_anvil_reviewer`", self.contract)
product_config = (ROOT / ".codex" / "config.toml").read_text(encoding="utf-8")
self.assertIn("[agents.plan_anvil_profiler]", product_config)
self.assertIn("[agents.plan_anvil_reviewer]", product_config)

def test_product_has_deterministic_mutation_postcondition(self) -> None:
self.assertIn("hook enforcement is an early guard only", self.contract)
Expand All @@ -50,6 +53,13 @@ def test_qualification_disables_token_budget_only_in_isolated_compaction_fixture
self.assertIn('_set_feature(text, "token_budget", "false")', self.compat)
self.assertIn("finally:\n v4._set_compact_config = old_set", self.compat)

def test_qualification_persists_project_trust_in_isolated_codex_home(self) -> None:
self.assertIn("_write_persisted_project_trust", self.compat)
self.assertIn('kwargs["trust_project"] = False', self.compat)
self.assertIn('item != "--ignore-user-config"', self.compat)
self.assertIn("_isolated_persisted_trust_runtime(cap_runtime)", self.compat)
self.assertIn('os.environ["CODEX_HOME"] = str(home)', self.compat)

def test_c06_qualifies_codex0152_guaranteed_hook_and_product_postcondition(self) -> None:
self.assertIn("C06_SUPPORTED_HOOK", self.compat)
self.assertIn('item.get("tool_name") == "Bash"', self.compat)
Expand Down
15 changes: 13 additions & 2 deletions tests/test_live_codex_qualification_harness_v6.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,31 @@ def test_agent_identity_is_aligned(self) -> None:
self.assertIn("required_spawn_agent_type", self.source)
self.assertIn("agent_type` exactly `fixture_agent`", self.compat)

def test_ephemeral_attempt_remains_project_scoped(self) -> None:
def test_ephemeral_attempt_remains_project_scoped_and_persistently_trusted(self) -> None:
self.assertIn("_seed_project_fixture(project_repo, proof, include_project_agent=True)", self.source)
self.assertIn("_prepare_trusted_ephemeral_home(cap_runtime, project_repo)", self.source)
self.assertIn("isolated_codex_home=ephemeral_home", self.source)
self.assertIn("ephemeral=True", self.source)
self.assertIn('trial_e["agent_fixture_scope"] = "project"', self.source)
self.assertIn('trial_e["persisted_project_trust"] = True', self.source)
self.assertIn("ephemeral_cleanup_verified", self.source)
self.assertIn("ephemeral_auth_metadata_unchanged", self.source)

def test_fallback_separates_agent_discovery_from_project_hook(self) -> None:
self.assertIn("_seed_project_fixture(fallback_repo, proof, include_project_agent=False)", self.source)
self.assertIn('home / "agents" / HOME_AGENT_FILENAME', self.source)
self.assertIn("compat._write_persisted_project_trust(home, repo)", self.source)
self.assertIn("_declare_home_agent(home)", self.source)
self.assertIn("_prepare_home_scoped_fallback_agent(cap_runtime, fallback_repo)", self.source)
self.assertIn('trial_n["agent_fixture_scope"] = "disposable_CODEX_HOME"', self.source)
self.assertIn('trial_n["project_agent_present"] = False', self.source)
self.assertIn('trial_n["project_scoped_subagent_start_hook"] = True', self.source)
self.assertIn('trial_n["persisted_project_trust"] = True', self.source)
self.assertIn("compat.run_c13(_c13_runtime", self.source)

def test_fallback_is_still_known_error_gated(self) -> None:
def test_known_ephemeral_parent_failure_always_uses_the_gated_fallback(self) -> None:
self.assertIn('if known_e:\n outcome_e = "BLOCKED"', self.source)
self.assertIn("recognized ephemeral parent-thread registration failure", self.source)
self.assertIn("known_e and ALLOW_NON_EPHEMERAL_FALLBACK", self.source)
self.assertIn("ephemeral_known_transport_blocker_fallback_not_enabled", self.source)
self.assertIn("non_ephemeral_home_agent_fallback", self.source)
Expand Down
94 changes: 90 additions & 4 deletions tools/live_codex_qualification_codex0152.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import contextlib
import json
import os
import re
from pathlib import Path
from typing import Any, Callable, Iterator
Expand Down Expand Up @@ -32,6 +33,81 @@ def _set_feature(text: str, key: str, value: str) -> str:
return text.rstrip() + f"\n\n[features]\n{key} = {value}\n"


def _write_persisted_project_trust(home: Path, repo: Path) -> None:
"""Persist the Codex 0.152 project trust decision in an isolated user config.

Codex 0.152 project trust is a user-config setting. Passing
`projects.<path>.trust_level` through `-c` is not a valid strict CLI override
and, without strict config, can be ignored. Qualification therefore mirrors
the supported product runtime by storing trust in the disposable CODEX_HOME.
"""

config_path = home / "config.toml"
text = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
text = _set_feature(text, "hooks", "true")
project_header = f"[projects.{base.toml_quote(str(repo.resolve()))}]"
if project_header not in text:
text = text.rstrip() + f"\n\n{project_header}\ntrust_level = \"trusted\"\n"
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(text.rstrip() + "\n", encoding="utf-8")


def _persisted_trust_args() -> Iterator[None]:
"""Route targeted 0.152 probes through persisted trust, never CLI trust."""

old_common = base.common_codex_args

def common_codex_args(**kwargs: Any) -> list[str]:
kwargs["trust_project"] = False
args = old_common(**kwargs)
return [item for item in args if item != "--ignore-user-config"]

base.common_codex_args = common_codex_args
try:
yield
finally:
base.common_codex_args = old_common


_persisted_trust_args = contextlib.contextmanager(_persisted_trust_args)


@contextlib.contextmanager
def _isolated_persisted_trust_runtime(cap_runtime: Path) -> Iterator[Path]:
"""Use one disposable CODEX_HOME and trust every probed cwd explicitly."""

home, auth_path, auth_before = v5._prepare_isolated_codex_home(
cap_runtime / "codex0152-persisted-trust"
)
old_common = base.common_codex_args
previous_home = os.environ.get("CODEX_HOME")

def common_codex_args(**kwargs: Any) -> list[str]:
cwd = Path(kwargs["cwd"]).resolve()
_write_persisted_project_trust(home, cwd)
kwargs["trust_project"] = False
args = old_common(**kwargs)
return [item for item in args if item != "--ignore-user-config"]

base.common_codex_args = common_codex_args
os.environ["CODEX_HOME"] = str(home)
try:
yield home
finally:
base.common_codex_args = old_common
if previous_home is None:
os.environ.pop("CODEX_HOME", None)
else:
os.environ["CODEX_HOME"] = previous_home
cleanup, auth_unchanged = v5._cleanup_isolated_codex_home(
home, auth_path, auth_before
)
if not cleanup or not auth_unchanged:
raise base.QualificationError(
"Codex 0.152 persisted-trust runtime did not clean up safely"
)


@contextlib.contextmanager
def _codex0152_compaction(cap_runtime: Path, capability_id: str) -> Iterator[None]:
"""Exercise real auto-compaction without the 0.152 TokenBudget fallback buffer.
Expand Down Expand Up @@ -60,13 +136,19 @@ def set_compact_config(repo: Path, *, limit: int, scope: str) -> None:

def run_c08(**kwargs: Any):
cap_runtime = Path(kwargs["runtime_root"]) / "C08"
with _codex0152_compaction(cap_runtime, "C08"):
with (
_codex0152_compaction(cap_runtime, "C08"),
_isolated_persisted_trust_runtime(cap_runtime),
):
return v4._c08_runtime(**kwargs)


def run_c09(**kwargs: Any):
cap_runtime = Path(kwargs["runtime_root"]) / "C09"
with _codex0152_compaction(cap_runtime, "C09"):
with (
_codex0152_compaction(cap_runtime, "C09"),
_isolated_persisted_trust_runtime(cap_runtime),
):
return v4._c09_runtime(**kwargs)


Expand Down Expand Up @@ -106,7 +188,7 @@ def _c13_contract(cap_runtime: Path) -> Iterator[None]:

def run_c13(current_runtime: Callable[..., tuple[str, bool]], **kwargs: Any):
cap_runtime = Path(kwargs["runtime_root"]) / "C13"
with _c13_contract(cap_runtime):
with _persisted_trust_args(), _c13_contract(cap_runtime):
return current_runtime(**kwargs)


Expand Down Expand Up @@ -144,7 +226,11 @@ def run_c06(
_cap, cap_runtime, _spec, repo, _worktrees, results, _eval = regression._runtime_paths(
root=root, runtime_root=runtime_root, capability_id=cid
)
with v2._python_bytecode_disabled(), regression._patched_v4(cap_runtime, cid):
with (
v2._python_bytecode_disabled(),
regression._patched_v4(cap_runtime, cid),
_isolated_persisted_trust_runtime(cap_runtime),
):
base.ensure_git_repo(repo)
regression.v1._install_plananvil_release(root, repo)
v4._instrument_hooks(repo, event_to_script={"PreToolUse": "plan-anvil-guard.py"})
Expand Down
Loading