diff --git a/code_puppy/plugins/chatgpt_oauth/IMAGEGEN_SKILL.md b/code_puppy/plugins/chatgpt_oauth/IMAGEGEN_SKILL.md index 54b086007..2db134d93 100644 --- a/code_puppy/plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +++ b/code_puppy/plugins/chatgpt_oauth/IMAGEGEN_SKILL.md @@ -3,6 +3,11 @@ name: codex-imagegen description: Generate raster images with gpt-image-2 through Codex OAuth. Activate when the user asks to create a photo, illustration, icon, sprite, texture, product image, banner, or other raster artwork. version: "1.0" author: code-puppy +tags: + - image-generation + - codex + - gpt-image + - artwork --- # Codex Image Generation diff --git a/code_puppy/plugins/chatgpt_oauth/register_callbacks.py b/code_puppy/plugins/chatgpt_oauth/register_callbacks.py index 850e1c5ea..555bfb4df 100644 --- a/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +++ b/code_puppy/plugins/chatgpt_oauth/register_callbacks.py @@ -14,6 +14,7 @@ from code_puppy.i18n import t from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning from code_puppy.model_switching import set_model_and_reload_agent +from code_puppy.plugins.agent_skills.discovery import refresh_skill_cache from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path from .oauth_flow import run_oauth_flow @@ -89,6 +90,10 @@ def _handle_chatgpt_logout() -> None: if was_authenticated: _reload_active_agent() + # Mirror the tool-unbinding reload above: the codex-imagegen skill is + # gated on auth too, so drop it from the skill cache immediately + # instead of leaving it visible until the next restart. + refresh_skill_cache() def _is_codex_oauth_authenticated() -> bool: @@ -125,6 +130,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if name in {"chatgpt-auth", "codex-auth"}: run_oauth_flow() set_model_and_reload_agent("codex-gpt-5.6-sol") + # Authentication may have just succeeded, so the codex-imagegen skill + # (gated the same way as the codex_imagegen tool) needs to be + # re-discovered rather than waiting for the next process restart. + refresh_skill_cache() return True if name in {"chatgpt-status", "codex-status"}: @@ -228,6 +237,13 @@ def _create_chatgpt_oauth_model( def _register_imagegen_skill() -> list[dict[str, str]]: + # Gated the same way as _advertise_imagegen_tool below: the skill's own + # instructions tell the model to call codex_imagegen(...), which doesn't + # exist as an available tool for an unauthenticated user. Hiding the + # skill itself avoids the confusing case where a model activates it, + # tries the tool call, and only then discovers auth is missing. + if not _is_codex_oauth_authenticated(): + return [] return [ { "name": "codex-imagegen", diff --git a/code_puppy/plugins/chatgpt_oauth/test_plugin.py b/code_puppy/plugins/chatgpt_oauth/test_plugin.py index 799b06e51..a055c4752 100644 --- a/code_puppy/plugins/chatgpt_oauth/test_plugin.py +++ b/code_puppy/plugins/chatgpt_oauth/test_plugin.py @@ -216,14 +216,14 @@ def test_codex_imagegen_command(): def test_imagegen_skill_and_tool_registration(): from code_puppy.plugins.chatgpt_oauth import register_callbacks - skills = register_callbacks._register_imagegen_skill() - assert skills[0]["name"] == "codex-imagegen" - assert Path(skills[0]["skill_md_path"]).is_file() with patch.object( register_callbacks, "load_stored_tokens", return_value={"access_token": "token", "account_id": "account"}, ): + skills = register_callbacks._register_imagegen_skill() + assert skills[0]["name"] == "codex-imagegen" + assert Path(skills[0]["skill_md_path"]).is_file() assert register_callbacks._advertise_imagegen_tool("code-puppy") == [ "codex_imagegen" ] @@ -233,6 +233,13 @@ def test_imagegen_skill_and_tool_registration(): ] +def test_imagegen_skill_is_not_registered_when_logged_out(): + from code_puppy.plugins.chatgpt_oauth import register_callbacks + + with patch.object(register_callbacks, "load_stored_tokens", return_value=None): + assert register_callbacks._register_imagegen_skill() == [] + + def test_imagegen_tool_is_not_advertised_when_logged_out(): from code_puppy.plugins.chatgpt_oauth import register_callbacks @@ -258,11 +265,52 @@ def test_logout_reloads_agent_to_unbind_imagegen(tmp_path): patch.object(register_callbacks, "emit_info"), patch.object(register_callbacks, "emit_success"), patch.object(register_callbacks, "_reload_active_agent") as reload_agent, + patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills, ): register_callbacks._handle_chatgpt_logout() assert not token_path.exists() reload_agent.assert_called_once_with() + refresh_skills.assert_called_once_with() + + +def test_logout_does_not_refresh_skills_when_already_logged_out(tmp_path): + from code_puppy.plugins.chatgpt_oauth import register_callbacks + + token_path = tmp_path / "tokens.json" + with ( + patch.object(register_callbacks, "load_stored_tokens", return_value=None), + patch.object( + register_callbacks, "get_token_storage_path", return_value=token_path + ), + patch.object(register_callbacks, "remove_chatgpt_models", return_value=0), + patch.object(register_callbacks, "emit_info"), + patch.object(register_callbacks, "emit_success"), + patch.object(register_callbacks, "_reload_active_agent") as reload_agent, + patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills, + ): + register_callbacks._handle_chatgpt_logout() + + reload_agent.assert_not_called() + refresh_skills.assert_not_called() + + +def test_auth_command_refreshes_skill_cache(): + from code_puppy.plugins.chatgpt_oauth import register_callbacks + + with ( + patch.object(register_callbacks, "run_oauth_flow") as oauth_flow, + patch.object(register_callbacks, "set_model_and_reload_agent") as set_model, + patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills, + ): + assert ( + register_callbacks._handle_custom_command("/codex-auth", "codex-auth") + is True + ) + + oauth_flow.assert_called_once_with() + set_model.assert_called_once_with("codex-gpt-5.6-sol") + refresh_skills.assert_called_once_with() def test_codex_imagegen_agent_tool(tmp_path):