From ce2d4c7b1e396ff626ded5f04bb6d165f16c27eb Mon Sep 17 00:00:00 2001 From: thaildhe172591 Date: Mon, 14 Sep 2026 14:09:02 +0700 Subject: [PATCH 1/2] fix: install no longer writes a config block that breaks Codex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.14.0 wrote an [approval_policy.granular] table with only mcp_elicitations, but that table requires several fields (sandbox_approval, rules, ...) and an incomplete one makes Codex v0.154 fail at config-load — the developer's Codex would not start. install now writes ONLY [mcp_servers.pythia]; elicitations surface under the default interactive approval policy, and the developer's approval policy is theirs to set. Verified against live Codex v0.154: the server connects, the pythia_approve elicitation renders its card with Approve/Reject, and Approve mints a grant (approver=mcp). requirements.toml is no longer written either — the danger-full-access pin became a printed warning, because that file is enterprise-managed and its schema is version-fragile in the same way that just broke config.toml. The server is fail-closed regardless. The pythia_approve success message now tells the agent to run apply --confirm itself on the file it previewed, instead of reading like a command for the developer to type by hand. Co-Authored-By: Claude Opus 4.8 --- scripts/pythia.py | 84 +++++++++++++++++++---------------------------- tests/test_mcp.py | 53 +++++++++++------------------- 2 files changed, 54 insertions(+), 83 deletions(-) diff --git a/scripts/pythia.py b/scripts/pythia.py index ebb40d9..275f370 100644 --- a/scripts/pythia.py +++ b/scripts/pythia.py @@ -3484,9 +3484,11 @@ def mcp_approve_decision(root, token, elicit): prune_expired_grants(root) mint_grant(root, token, meta.get("connection") or "", revalidate=fingerprint_text(meta.get("row_set")), approver="mcp") - return _mcp_text(f"Approved — grant minted for {token}, single use, expires " - f"in {GRANT_TTL_MINUTES} minutes. Now run: " - f"{invocation()} apply --confirm {token}") + return _mcp_text(f"Approved — grant minted for {token} (single use, " + f"{GRANT_TTL_MINUTES} min). Now run apply --confirm on the " + "file you previewed YOURSELF — do not hand the developer a " + f"command to type: {invocation()} apply --confirm " + f"{token}") def mcp_serve(reader, writer, root): @@ -3558,33 +3560,30 @@ def cmd_mcp(conn, schema, ns): # table header inside a string/comment is not worth a parser here. CODEX_MCP_BEGIN = "# pythia:begin (managed by `pythia install`)" CODEX_MCP_END = "# pythia:end" +# Only the server roster — nothing else. An earlier attempt also wrote an +# [approval_policy.granular] table, but that table requires several fields +# (sandbox_approval, rules, ...) and an incomplete one broke Codex v0.154 at +# config-load time. Elicitations surface under the default interactive +# approval policy without any of that, and the developer's approval policy is +# theirs, not ours to set. CODEX_MCP_BLOCK = (f"{CODEX_MCP_BEGIN}\n" "[mcp_servers.pythia]\n" 'command = "python"\n' - 'args = ["-m", "pythia", "mcp"]\n\n' - "[approval_policy.granular]\n" - "mcp_elicitations = true\n" + 'args = ["-m", "pythia", "mcp"]\n' f"{CODEX_MCP_END}") -# requirements.toml is a separate admin-enforced file; forbidding these two is -# what closes the danger-full-access elicitation auto-approve hole. -CODEX_REQUIREMENTS = (f"{CODEX_MCP_BEGIN}\n" - "# pythia needs elicitations to reach a human, so the two\n" - "# modes that would auto-answer or skip them are disallowed.\n" - 'allowed_approval_policies = ["untrusted", "on-request", "on-failure"]\n' - 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n' - f"{CODEX_MCP_END}") def merge_codex_mcp_config(path): - """Append the MCP block to /.codex/config.toml. Returns - (path, action): 'created' | 'appended' | 'present' | 'conflict'. Refuses - (writes nothing) if either table already exists without our marker.""" + """Append the MCP server registration to /.codex/config.toml. + Returns (path, action): 'created' | 'appended' | 'present' | 'conflict'. + Refuses (writes nothing) if [mcp_servers.pythia] already exists without our + marker — the stdlib cannot safely merge TOML, so a collision is reported, + never overwritten.""" path = pathlib.Path(path) old = path.read_text(encoding="utf-8") if path.is_file() else None if old is not None and CODEX_MCP_BEGIN in old: return path, "present" - if old is not None and ("[mcp_servers.pythia]" in old - or "[approval_policy.granular]" in old): + if old is not None and "[mcp_servers.pythia]" in old: return path, "conflict" if old is None: new, action = CODEX_MCP_BLOCK + "\n", "created" @@ -3596,21 +3595,6 @@ def merge_codex_mcp_config(path): return path, action -def merge_codex_requirements(path): - """Write /.codex/requirements.toml forbidding danger-full-access - and never-approval. Returns (path, action): 'created' | 'present' | - 'conflict'. An existing file without our marker is left untouched.""" - path = pathlib.Path(path) - old = path.read_text(encoding="utf-8") if path.is_file() else None - if old is not None and CODEX_MCP_BEGIN in old: - return path, "present" - if old is not None: - return path, "conflict" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(CODEX_REQUIREMENTS + "\n", encoding="utf-8") - return path, "created" - - def install_claude_hooks(base_dir, events=None): """Merge the two hooks and the deny rule into /.claude/settings.json — the project's, or the home directory's for -g. The file is Claude @@ -3693,28 +3677,28 @@ def report_codex_hooks(project_root, ns): def report_codex_mcp(project_root, ns): - """Project scope: register the write-free MCP approver in Codex's config and - pin the sandbox so elicitations always reach a human. --no-hooks skips it, - as with the other Codex wiring. Needs Codex >= v0.120; older Codex ignores - the server and the console approve path still works.""" + """Project scope: register the write-free MCP approver in Codex's config. + --no-hooks skips it, as with the other Codex wiring. Needs Codex >= v0.120 + for elicitation; older Codex ignores the server and the console approve + path still works.""" if getattr(ns, "no_hooks", False): return - base = pathlib.Path(project_root) / ".codex" - cfg_path, cfg = merge_codex_mcp_config(base / "config.toml") - req_path, req = merge_codex_requirements(base / "requirements.toml") + cfg_path, cfg = merge_codex_mcp_config(pathlib.Path(project_root) + / ".codex" / "config.toml") if cfg in ("created", "appended"): print(f"\nRegistered the pythia MCP approver in {cfg_path} " "(Codex >= v0.120; run /mcp to trust it).") elif cfg == "conflict": - print(f"\n! {cfg_path} already defines [mcp_servers.pythia] or " - "[approval_policy.granular]; add this yourself:\n" - + CODEX_MCP_BLOCK) - if req == "created": - print(f"Pinned the sandbox in {req_path} (no danger-full-access, so " - "elicitations always ask a human).") - elif req == "conflict": - print(f"\n! {req_path} already exists; add these constraints to it:\n" - + CODEX_REQUIREMENTS) + print(f"\n! {cfg_path} already defines [mcp_servers.pythia]; add this " + "yourself:\n" + CODEX_MCP_BLOCK) + else: + print(f"\npythia MCP approver already registered in {cfg_path}.") + # The one bypass of the elicitation is Codex auto-approving it under the + # danger-full-access sandbox. pythia does not write requirements.toml to + # forbid that — it is enterprise-managed config, and the schema is + # version-fragile — so it warns instead. + print("Do not run Codex under `danger-full-access` with pythia: that " + "sandbox auto-approves elicitations, bypassing your Approve.") def cmd_install(conn, schema, ns): diff --git a/tests/test_mcp.py b/tests/test_mcp.py index e3c9981..1af226f 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -79,7 +79,11 @@ def elicit(message, schema): res = pythia.mcp_approve_decision(td, token, elicit) assert res["isError"] is False - assert token in res["content"][0]["text"] + text = res["content"][0]["text"] + assert token in text + # the message tells the AGENT to continue itself, not the developer to + # type a command — 0.14.1 fix + assert "yourself" in text.lower() assert "PKG_ORDER" in seen["message"] # the card was shown assert seen["schema"]["properties"]["decision"]["enum"] == ["Approve", "Reject"] g = pythia.read_grant(td, token) @@ -200,7 +204,10 @@ def test_merge_codex_mcp_config_appends_once_when_clean(): body = path.read_text(encoding="utf-8") assert "[mcp_servers.pythia]" in body assert 'args = ["-m", "pythia", "mcp"]' in body - assert "mcp_elicitations = true" in body + # 0.14.1: we do NOT touch approval_policy — an incomplete granular table + # broke Codex v0.154 config loading, and elicitations work under the + # default interactive policy anyway. + assert "approval_policy" not in body assert pythia.merge_codex_mcp_config(path)[1] == "present" # idempotent @@ -216,43 +223,20 @@ def test_merge_codex_mcp_config_preserves_other_content(): assert "[mcp_servers.pythia]" in body -def test_merge_codex_mcp_config_refuses_on_table_collision(): +def test_merge_codex_mcp_config_refuses_on_server_collision(): with tempfile.TemporaryDirectory() as td: path = pathlib.Path(td) / ".codex" / "config.toml" path.parent.mkdir(parents=True) - original = "[approval_policy.granular]\nmcp_elicitations = false\n" + original = '[mcp_servers.pythia]\ncommand = "other"\n' path.write_text(original, encoding="utf-8") p, action = pythia.merge_codex_mcp_config(path) assert action == "conflict" assert path.read_text(encoding="utf-8") == original # untouched -def test_merge_codex_requirements_creates_and_is_idempotent(): - with tempfile.TemporaryDirectory() as td: - path = pathlib.Path(td) / ".codex" / "requirements.toml" - p, action = pythia.merge_codex_requirements(path) - assert action == "created" - body = path.read_text(encoding="utf-8") - assert "allowed_approval_policies" in body and "allowed_sandbox_modes" in body - assert "danger-full-access" not in body # the forbidden mode is absent - assert '"never"' not in body # the forbidden policy is absent - assert pythia.merge_codex_requirements(path)[1] == "present" - - -def test_merge_codex_requirements_refuses_an_existing_file(): - with tempfile.TemporaryDirectory() as td: - path = pathlib.Path(td) / ".codex" / "requirements.toml" - path.parent.mkdir(parents=True) - original = "# org policy\nallowed_sandbox_modes = []\n" - path.write_text(original, encoding="utf-8") - p, action = pythia.merge_codex_requirements(path) - assert action == "conflict" - assert path.read_text(encoding="utf-8") == original # untouched - - # --- Task 5: wire the MCP install into cmd_install (project scope) ---------- -def test_cmd_install_registers_the_mcp_server_and_pins_the_sandbox(): +def test_cmd_install_registers_the_mcp_server_and_warns_off_danger(): import argparse import contextlib old = os.environ.get("PATH") @@ -261,13 +245,16 @@ def test_cmd_install_registers_the_mcp_server_and_pins_the_sandbox(): with tempfile.TemporaryDirectory() as td: ns = argparse.Namespace(project_root=td, glob=False, source=None, color=False, json=False, no_hooks=False) - with contextlib.redirect_stdout(io.StringIO()): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): pythia.cmd_install(None, None, ns) cfg = pathlib.Path(td) / ".codex" / "config.toml" - req = pathlib.Path(td) / ".codex" / "requirements.toml" - assert "[mcp_servers.pythia]" in cfg.read_text(encoding="utf-8") - assert "mcp_elicitations = true" in cfg.read_text(encoding="utf-8") - assert "allowed_sandbox_modes" in req.read_text(encoding="utf-8") + body = cfg.read_text(encoding="utf-8") + assert "[mcp_servers.pythia]" in body + assert "approval_policy" not in body # 0.14.1: never touched + # requirements.toml is no longer written; the pin became a warning + assert not (pathlib.Path(td) / ".codex" / "requirements.toml").exists() + assert "danger-full-access" in buf.getvalue() # the warning finally: if old is not None: os.environ["PATH"] = old From 3add8b5956caaadbaeb577104e9bd1ef41aa0332 Mon Sep 17 00:00:00 2001 From: thaildhe172591 Date: Mon, 14 Sep 2026 14:09:44 +0700 Subject: [PATCH 2/2] =?UTF-8?q?0.14.1=20=E2=80=94=20install=20no=20longer?= =?UTF-8?q?=20writes=20a=20config=20block=20that=20breaks=20Codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ npm/package.json | 2 +- pyproject.toml | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 26b2f63..06bf843 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,7 +5,7 @@ }, "metadata": { "description": "PL/SQL development kit for AI agents on Oracle Database", - "version": "0.14.0" + "version": "0.14.1" }, "plugins": [ { diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ee42e4b..76c85cb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pythia", - "version": "0.14.0", + "version": "0.14.1", "description": "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.", "author": { "name": "thaild" diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f0cc6..43ee9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.14.1 — 2026-09-14 + +- **install no longer breaks Codex's config.** 0.14.0 wrote an + `[approval_policy.granular]` table carrying only `mcp_elicitations = true`, + but that table requires several fields on Codex v0.154 (`sandbox_approval`, + `rules`, …) and an incomplete one fails at config-load — the developer's + Codex would not start (`missing field sandbox_approval`). Seen the first + time 0.14.0 was installed against a real Codex. install now writes **only** + `[mcp_servers.pythia]`; MCP elicitations surface under the default + interactive approval policy without any granular table, and the approval + policy is the developer's to set, not pythia's. The rest of 0.14.0 is + confirmed working against live Codex v0.154: the server connects, the + `pythia_approve` elicitation renders its card with Approve/Reject, and + Approve mints a grant (`approver="mcp"`). +- **requirements.toml is no longer written.** The danger-full-access pin + became a printed warning instead. That file is enterprise-managed config and + its schema is version-fragile in exactly the way that just broke + `config.toml`; the MCP approver is fail-closed regardless, and the + elicitation asks for an explicit Approve. +- The `pythia_approve` result now tells the agent to run `apply --confirm` + itself on the file it previewed, rather than reading like a command for the + developer to type by hand. + ## 0.14.0 — 2026-09-14 - **The Claude approve-and-execute experience, now on Codex.** 0.13.0 put the diff --git a/npm/package.json b/npm/package.json index 209e8a4..360ed5a 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "pythia-plsql", - "version": "0.14.0", + "version": "0.14.1", "description": "One-command installer for pythia: PL/SQL development for AI agents on Oracle Database. Installs the Python CLI, the skill pack, and the .pythia/ config scaffold.", "bin": { "pythia-plsql": "bin.js" diff --git a/pyproject.toml b/pyproject.toml index a756c9b..0715ca7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pythia-plsql" -version = "0.14.0" +version = "0.14.1" description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback." readme = "README.md" license = { text = "MIT" }