{"repo":"NousResearch/hermes-agent","public_id":"39187","display_label":"execute_code approval persistence and guard checks","summary":"These issues and fixes address a bug where 'Approve for session' and 'Always' approvals for execute_code were not being persisted or checked correctly, causing the approval prompt to reappear on every call. The fixes involve checking is_approved() and the permanent allowlist in the execute_code guard.","subsystem":"agent","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":39466},"resolution":{"state":"partially_resolved","by":"pr-39466"},"freshness":{"state":"verified","last_verified_at":"2026-06-05T02:40:30+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3028571428571429},"fix_edges":[{"issue_number":39187,"pr_number":39466,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":39275,"pr_number":39466,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":39187,"title":"Bug: execute_code approval \"Always\" (permanent) doesn't persist — popup reappears on every call","url":null,"state":"closed"},{"kind":"issue","number":39275,"title":"execute_code approval: 'Approve session' / 'Always' buttons do not work (missing is_approved check)","url":null,"state":"closed"},{"kind":"pr","number":39204,"title":"fix(approval): check permanent allowlist in execute_code guard","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"25742372eb5664bc9626143a486823b27fb4bc5e","number":0},{"kind":"pr","value":"#39275","number":39275}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":39212,"title":"fix(tools): honor execute code permanent approvals","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"25742372eb5664bc9626143a486823b27fb4bc5e","number":0},{"kind":"pr","value":"#39466","number":39466},{"kind":"pr","value":"#39466","number":39466}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":39391,"title":"fix(tools): check is_approved() before prompting in execute_code guard","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#39466","number":39466}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":39413,"title":"fix(approval): check is_approved in execute_code guard (#39275)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"25742372e","number":0},{"kind":"pr","value":"#39466","number":39466},{"kind":"pr","value":"#39275","number":39275}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":39466,"title":"fix(approval): persist execute_code session/always approvals (#39275)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_execute_code_approval_cluster.py","tools/approval.py"],"fix_hunks":[{"file":"tests/tools/test_execute_code_approval_cluster.py","hunks":["@@ -176,6 +176,53 @@ def test_guard_gateway_user_approves_is_one_shot(gw_session):\n     assert A.is_approved(gw_session, \"execute_code\") is False\n \n \n+def test_guard_gateway_user_approves_session_persists(gw_session):\n+    \"\"\"'Approve session' stores session-level approval (#39275).\"\"\"\n+    _register_resolver(gw_session, \"session\")\n+    res = A.check_execute_code_guard(\"import os; print(1)\", \"local\")\n+    assert res[\"approved\"] is True\n+    assert res.get(\"user_approved\") is True\n+    # Session approval should now be stored.\n+    assert A.is_approved(gw_session, \"execute_code\") is True\n+    # Subsequent calls should auto-approve without prompting.\n+    res2 = A.check_execute_code_guard(\"import os; print(2)\", \"local\")\n+    assert res2[\"approved\"] is True\n+    # Cleanup\n+    with A._lock:\n+        s = A._session_approved.get(gw_session, set())\n+        s.discard(\"execute_code\")\n+\n+\n+def test_guard_gateway_user_approves_always_persists(gw_session):\n+    \"\"\"'Always' stores permanent approval (#39275).\"\"\"\n+    _register_resolver(gw_session, \"always\")\n+    res = A.check_execute_code_guard(\"import os; print(1)\", \"local\")\n+    assert res[\"approved\"] is True\n+    assert res.get(\"user_approved\") is True\n+    # Permanent approval should now be stored.\n+    assert A.is_approved(gw_session, \"execute_code\") is True\n+    # Cleanup\n+    with A._lock:\n+        A._permanent_approved.discard(\"execute_code\")\n+        s = A._session_approved.get(gw_session, set())\n+        s.discard(\"execute_code\")\n+\n+\n+def test_guard_session_approval_short_circuits_prompt(gw_session):\n+    \"\"\"Once session-approved, execute_code skips the approval prompt (#39275).\"\"\"\n+    # Manually set session approval.\n+    A.approve_session(gw_session, \"execute_code\")\n+    try:\n+        # Even with a denier registered, the is_approved check short-circuits.\n+        _register_resolver(gw_session, \"deny\")\n+        res = A.check_execute_code_guard(\"import os\", \"local\")\n+        assert res[\"approved\"] is True\n+    finally:\n+        with A._lock:\n+            s = A._session_approved.get(gw_session, set())\n+            s.discard(\"execute_code\")\n+\n+\n def test_guard_gateway_user_denies_blocks(gw_session):\n     _register_resolver(gw_session, \"deny\")\n     res = A.check_execute_code_guard(\"import os\", \"local\")"]},{"file":"tools/approval.py","hunks":["@@ -1584,6 +1584,12 @@ def check_execute_code_guard(code: str, env_type: str) -> dict:\n     # paths don't pay to copy a potentially-large script into this string.\n     command = f\"execute_code <<'PY'\\n{code}\\nPY\"\n \n+    # Check session/permanent approval — same gate as check_all_command_guards.\n+    # Without this, \"Approve session\" / \"Always\" choices are stored but never\n+    # consulted, so every execute_code call re-prompts the user (#39275).\n+    if is_approved(session_key, pattern_key):\n+        return {\"approved\": True, \"message\": None}\n+\n     # Smart mode: ask the aux LLM about the whole script. An APPROVE here only\n     # suppresses the redundant whole-script prompt; the per-call terminal()\n     # guards (restored by context propagation) still run independently.","@@ -1674,9 +1680,15 @@ def check_execute_code_guard(code: str, env_type: str) -> dict:\n             \"user_consent\": False,\n         }\n \n-    # Approved — one-shot only. Deliberately NO approve_session/approve_permanent:\n-    # each execute_code script is distinct arbitrary code, so approval never\n-    # persists to future scripts.\n+    # Approved — persist based on scope (same logic as check_all_command_guards).\n+    if choice == \"session\":\n+        approve_session(session_key, pattern_key)\n+    elif choice == \"always\":\n+        approve_session(session_key, pattern_key)\n+        approve_permanent(pattern_key)\n+        save_permanent_allowlist(_permanent_approved)\n+    # choice == \"once\": no persistence — approval lasts this single call only.\n+\n     return {\"approved\": True, \"message\": None,\n             \"user_approved\": True, \"description\": description}\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}