diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7a2a0..e849a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Chock changelog +## Unreleased + +- **The plugins page states each client's own answer to a crashed guard.** Every tree's + `PLUGINS.md` said a crashed guard "asks -- on Codex CLI, denies", naming Codex on the Cursor, + Copilot and Devin pages alike. The sentence is now taken from the tested `honours_ask` claim for + the tree's client: an ask where the client prompts (Claude Code, Cursor, VS Code Copilot), a + refusal where it cannot (Codex CLI, Devin), and an untested ask where chock holds no tested claim. + ## 0.11.1 — The plugins page tells a gate from a guard - **The generated plugins page tells a gate from a guard.** `chock marketplace build` wrote one diff --git a/src/chock/plugin/catalog_page.py b/src/chock/plugin/catalog_page.py index 6c8bf56..d68be22 100644 --- a/src/chock/plugin/catalog_page.py +++ b/src/chock/plugin/catalog_page.py @@ -13,7 +13,7 @@ from agentseam import packaging -from chock import vendors +from chock import evidence, vendors from chock.plugin.marketplace_core import CLAUDE_TREE, NEWLINE, _manifest_rel from chock.vendors import CHOCK_AGENT @@ -96,6 +96,16 @@ def _event_list(events: list[str]) -> str: return quoted[0] if len(quoted) == 1 else ", ".join(quoted[:-1]) + " and " + quoted[-1] +def _on_crash(tree: str) -> str: + """What this client does with a crashed guard's ask, per chock's tested claim for it.""" + row = evidence.claim(CHOCK_AGENT[tree], evidence.HONOURS_ASK) + if row is None or row.evidence != evidence.TESTED: + return "returns an ask this client has not been tested to honour" + if row.honours: + return "asks for confirmation rather than allowing silently" + return "refuses the command: this client cannot prompt for confirmation" + + def _explain(tree: str, guards: int, guard_events: list[str], gates: int, gate_events: list[str]) -> str: """What an enforcing package here ships and does, derived from what was published.""" parts: list[str] = [] @@ -103,8 +113,8 @@ def _explain(tree: str, guards: int, guard_events: list[str], gates: int, gate_e parts.append( f"A guard package ships a guard script and a stdlib-only adapter, hooked at " f"{_event_list(guard_events)}, and can deny a shell command before the client runs it. " - "It fails open when `python3` or a usable `bash` is unavailable, and asks -- on Codex " - "CLI, denies -- when the guard crashes." + "It fails open when `python3` or a usable `bash` is unavailable. When the guard itself " + f"crashes, the hook {_on_crash(tree)}." ) if gates: judges_write = vendors.pre_tool_event(CHOCK_AGENT[tree]) in gate_events diff --git a/tests/test_catalog_page.py b/tests/test_catalog_page.py index d3d811c..4178c62 100644 --- a/tests/test_catalog_page.py +++ b/tests/test_catalog_page.py @@ -97,3 +97,20 @@ def test_catalog_page_names_each_client_s_own_events(gate_dist: Path) -> None: assert "hooked at `beforeShellExecution`" in body assert "hooked at `preToolUse` and `stop`" in body assert "`PreToolUse`" not in body + + +@pytest.mark.parametrize( + ("tree", "on_crash"), + [ + ("claude", "the hook asks for confirmation rather than allowing silently."), + ("cursor", "the hook asks for confirmation rather than allowing silently."), + ("codex", "the hook refuses the command: this client cannot prompt for confirmation."), + ], +) +def test_catalog_page_states_this_client_s_own_crash_answer(gate_dist: Path, tree: str, on_crash: str) -> None: + """A crashed guard's answer comes from the tested claim for this client, not another's.""" + marketplace_main(["build", "--dist", str(gate_dist), "--tree", tree]) + body = (gate_dist / CATALOG_PAGE).read_text(encoding="utf-8") + + assert f"When the guard itself crashes, {on_crash}" in body + assert "Codex CLI" not in body