code-puppy 0.0.320__tar.gz → 0.0.323__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_puppy-0.0.320 → code_puppy-0.0.323}/PKG-INFO +1 -1
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/base_agent.py +154 -13
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/cli_runner.py +2 -1
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/keymap.py +8 -2
- code_puppy-0.0.323/code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/command_runner.py +48 -21
- {code_puppy-0.0.320 → code_puppy-0.0.323}/pyproject.toml +1 -1
- code_puppy-0.0.320/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -186
- {code_puppy-0.0.320 → code_puppy-0.0.323}/.gitignore +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/LICENSE +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/README.md +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_c_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_code_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_golang_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_planning.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_python_programmer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_python_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_expert.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_security_auditor.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/prompt_reviewer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/chatgpt_codex_client.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/claude_cache_client.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/add_model_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/attachments.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/autosave_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/colors_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/command_registry.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/config_commands.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/core_commands.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/diff_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/edit_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/model_settings_menu.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/pin_command_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/session_commands.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/config.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/error_logging.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/gemini_code_assist.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/main.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/captured_stdio_server.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/managed_server.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/manager.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/mcp_logs.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/bus.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/commands.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/markdown_patches.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/messages.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/rich_renderer.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/model_utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models.json +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models_dev_parser.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/README.md +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/oauth_puppy_html.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/prompts/codex_system_prompt.md +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/pydantic_patches.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/session_storage.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/terminal_utils.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/camoufox_manager.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/vqa_agent.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/version_checker.py +0 -0
|
@@ -4,11 +4,23 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import math
|
|
6
6
|
import signal
|
|
7
|
+
import sys
|
|
7
8
|
import threading
|
|
8
9
|
import uuid
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
from collections.abc import AsyncIterable
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Dict,
|
|
16
|
+
List,
|
|
17
|
+
Optional,
|
|
18
|
+
Sequence,
|
|
19
|
+
Set,
|
|
20
|
+
Tuple,
|
|
21
|
+
Type,
|
|
22
|
+
Union,
|
|
23
|
+
)
|
|
12
24
|
|
|
13
25
|
import mcp
|
|
14
26
|
import pydantic
|
|
@@ -1230,6 +1242,74 @@ class BaseAgent(ABC):
|
|
|
1230
1242
|
self._mcp_servers = mcp_servers
|
|
1231
1243
|
return self._code_generation_agent
|
|
1232
1244
|
|
|
1245
|
+
def _create_agent_with_output_type(self, output_type: Type[Any]) -> PydanticAgent:
|
|
1246
|
+
"""Create a temporary agent configured with a custom output_type.
|
|
1247
|
+
|
|
1248
|
+
This is used when structured output is requested via run_with_mcp.
|
|
1249
|
+
The agent is created fresh with the same configuration as the main agent
|
|
1250
|
+
but with the specified output_type instead of str.
|
|
1251
|
+
|
|
1252
|
+
Args:
|
|
1253
|
+
output_type: The Pydantic model or type for structured output.
|
|
1254
|
+
|
|
1255
|
+
Returns:
|
|
1256
|
+
A configured PydanticAgent (or DBOSAgent wrapper) with the custom output_type.
|
|
1257
|
+
"""
|
|
1258
|
+
from code_puppy.model_utils import prepare_prompt_for_model
|
|
1259
|
+
from code_puppy.tools import register_tools_for_agent
|
|
1260
|
+
|
|
1261
|
+
model_name = self.get_model_name()
|
|
1262
|
+
models_config = ModelFactory.load_config()
|
|
1263
|
+
model, resolved_model_name = self._load_model_with_fallback(
|
|
1264
|
+
model_name, models_config, str(uuid.uuid4())
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
instructions = self.get_system_prompt()
|
|
1268
|
+
puppy_rules = self.load_puppy_rules()
|
|
1269
|
+
if puppy_rules:
|
|
1270
|
+
instructions += f"\n{puppy_rules}"
|
|
1271
|
+
|
|
1272
|
+
mcp_servers = getattr(self, "_mcp_servers", []) or []
|
|
1273
|
+
model_settings = make_model_settings(resolved_model_name)
|
|
1274
|
+
|
|
1275
|
+
prepared = prepare_prompt_for_model(
|
|
1276
|
+
model_name, instructions, "", prepend_system_to_user=False
|
|
1277
|
+
)
|
|
1278
|
+
instructions = prepared.instructions
|
|
1279
|
+
|
|
1280
|
+
global _reload_count
|
|
1281
|
+
_reload_count += 1
|
|
1282
|
+
|
|
1283
|
+
if get_use_dbos():
|
|
1284
|
+
temp_agent = PydanticAgent(
|
|
1285
|
+
model=model,
|
|
1286
|
+
instructions=instructions,
|
|
1287
|
+
output_type=output_type,
|
|
1288
|
+
retries=3,
|
|
1289
|
+
toolsets=[],
|
|
1290
|
+
history_processors=[self.message_history_accumulator],
|
|
1291
|
+
model_settings=model_settings,
|
|
1292
|
+
)
|
|
1293
|
+
agent_tools = self.get_available_tools()
|
|
1294
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
1295
|
+
dbos_agent = DBOSAgent(
|
|
1296
|
+
temp_agent, name=f"{self.name}-structured-{_reload_count}"
|
|
1297
|
+
)
|
|
1298
|
+
return dbos_agent
|
|
1299
|
+
else:
|
|
1300
|
+
temp_agent = PydanticAgent(
|
|
1301
|
+
model=model,
|
|
1302
|
+
instructions=instructions,
|
|
1303
|
+
output_type=output_type,
|
|
1304
|
+
retries=3,
|
|
1305
|
+
toolsets=mcp_servers,
|
|
1306
|
+
history_processors=[self.message_history_accumulator],
|
|
1307
|
+
model_settings=model_settings,
|
|
1308
|
+
)
|
|
1309
|
+
agent_tools = self.get_available_tools()
|
|
1310
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
1311
|
+
return temp_agent
|
|
1312
|
+
|
|
1233
1313
|
# It's okay to decorate it with DBOS.step even if not using DBOS; the decorator is a no-op in that case.
|
|
1234
1314
|
@DBOS.step()
|
|
1235
1315
|
def message_history_accumulator(self, ctx: RunContext, messages: List[Any]):
|
|
@@ -1590,6 +1670,7 @@ class BaseAgent(ABC):
|
|
|
1590
1670
|
*,
|
|
1591
1671
|
attachments: Optional[Sequence[BinaryContent]] = None,
|
|
1592
1672
|
link_attachments: Optional[Sequence[Union[ImageUrl, DocumentUrl]]] = None,
|
|
1673
|
+
output_type: Optional[Type[Any]] = None,
|
|
1593
1674
|
**kwargs,
|
|
1594
1675
|
) -> Any:
|
|
1595
1676
|
"""Run the agent with MCP servers, attachments, and full cancellation support.
|
|
@@ -1598,10 +1679,13 @@ class BaseAgent(ABC):
|
|
|
1598
1679
|
prompt: Primary user prompt text (may be empty when attachments present).
|
|
1599
1680
|
attachments: Local binary payloads (e.g., dragged images) to include.
|
|
1600
1681
|
link_attachments: Remote assets (image/document URLs) to include.
|
|
1682
|
+
output_type: Optional Pydantic model or type for structured output.
|
|
1683
|
+
When provided, creates a temporary agent configured to return
|
|
1684
|
+
this type instead of the default string output.
|
|
1601
1685
|
**kwargs: Additional arguments forwarded to `pydantic_ai.Agent.run`.
|
|
1602
1686
|
|
|
1603
1687
|
Returns:
|
|
1604
|
-
The agent's response.
|
|
1688
|
+
The agent's response (typed according to output_type if specified).
|
|
1605
1689
|
|
|
1606
1690
|
Raises:
|
|
1607
1691
|
asyncio.CancelledError: When execution is cancelled by user.
|
|
@@ -1625,6 +1709,11 @@ class BaseAgent(ABC):
|
|
|
1625
1709
|
pydantic_agent = (
|
|
1626
1710
|
self._code_generation_agent or self.reload_code_generation_agent()
|
|
1627
1711
|
)
|
|
1712
|
+
|
|
1713
|
+
# If a custom output_type is specified, create a temporary agent with that type
|
|
1714
|
+
if output_type is not None:
|
|
1715
|
+
pydantic_agent = self._create_agent_with_output_type(output_type)
|
|
1716
|
+
|
|
1628
1717
|
# Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
|
|
1629
1718
|
from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
|
|
1630
1719
|
|
|
@@ -1822,29 +1911,72 @@ class BaseAgent(ABC):
|
|
|
1822
1911
|
# When using keyboard-based cancel, SIGINT should be a no-op
|
|
1823
1912
|
# (just show a hint to user about the configured cancel key)
|
|
1824
1913
|
from code_puppy.keymap import get_cancel_agent_display_name
|
|
1914
|
+
import sys
|
|
1825
1915
|
|
|
1826
1916
|
cancel_key = get_cancel_agent_display_name()
|
|
1827
|
-
|
|
1917
|
+
if sys.platform == "win32":
|
|
1918
|
+
# On Windows, we use keyboard listener, so SIGINT might still fire
|
|
1919
|
+
# but we handle cancellation via the key listener
|
|
1920
|
+
pass # Silent on Windows - the key listener handles it
|
|
1921
|
+
else:
|
|
1922
|
+
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1828
1923
|
|
|
1829
1924
|
original_handler = None
|
|
1830
1925
|
key_listener_stop_event = None
|
|
1831
1926
|
_key_listener_thread = None
|
|
1927
|
+
_windows_ctrl_handler = None # Store reference to prevent garbage collection
|
|
1832
1928
|
|
|
1833
1929
|
try:
|
|
1834
|
-
if
|
|
1835
|
-
# Use
|
|
1930
|
+
if sys.platform == "win32":
|
|
1931
|
+
# Windows: Use SetConsoleCtrlHandler for reliable Ctrl+C handling
|
|
1932
|
+
import ctypes
|
|
1933
|
+
|
|
1934
|
+
# Define the handler function type
|
|
1935
|
+
HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
|
|
1936
|
+
|
|
1937
|
+
def windows_ctrl_handler(ctrl_type):
|
|
1938
|
+
"""Handle Windows console control events."""
|
|
1939
|
+
CTRL_C_EVENT = 0
|
|
1940
|
+
CTRL_BREAK_EVENT = 1
|
|
1941
|
+
|
|
1942
|
+
if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
|
|
1943
|
+
# Check if we're awaiting user input
|
|
1944
|
+
if is_awaiting_user_input():
|
|
1945
|
+
return False # Let default handler run
|
|
1946
|
+
|
|
1947
|
+
# Schedule agent cancellation
|
|
1948
|
+
schedule_agent_cancel()
|
|
1949
|
+
return True # We handled it, don't terminate
|
|
1950
|
+
|
|
1951
|
+
return False # Let other handlers process it
|
|
1952
|
+
|
|
1953
|
+
# Create the callback - must keep reference alive!
|
|
1954
|
+
_windows_ctrl_handler = HANDLER_ROUTINE(windows_ctrl_handler)
|
|
1955
|
+
|
|
1956
|
+
# Register the handler
|
|
1957
|
+
kernel32 = ctypes.windll.kernel32
|
|
1958
|
+
if not kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, True):
|
|
1959
|
+
emit_warning("Failed to set Windows Ctrl+C handler")
|
|
1960
|
+
|
|
1961
|
+
# Also spawn keyboard listener for Ctrl+X (shell cancel) and other keys
|
|
1962
|
+
key_listener_stop_event = threading.Event()
|
|
1963
|
+
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1964
|
+
key_listener_stop_event,
|
|
1965
|
+
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
1966
|
+
on_cancel_agent=None, # Ctrl+C handled by SetConsoleCtrlHandler above
|
|
1967
|
+
)
|
|
1968
|
+
elif cancel_agent_uses_signal():
|
|
1969
|
+
# Unix with Ctrl+C: Use SIGINT-based cancellation
|
|
1836
1970
|
original_handler = signal.signal(
|
|
1837
1971
|
signal.SIGINT, keyboard_interrupt_handler
|
|
1838
1972
|
)
|
|
1839
1973
|
else:
|
|
1840
|
-
# Use keyboard listener
|
|
1841
|
-
# Set a graceful SIGINT handler that shows a hint
|
|
1974
|
+
# Unix with different cancel key: Use keyboard listener
|
|
1842
1975
|
original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
|
|
1843
|
-
# Spawn keyboard listener with the cancel agent callback
|
|
1844
1976
|
key_listener_stop_event = threading.Event()
|
|
1845
1977
|
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1846
1978
|
key_listener_stop_event,
|
|
1847
|
-
on_escape=lambda: None,
|
|
1979
|
+
on_escape=lambda: None,
|
|
1848
1980
|
on_cancel_agent=schedule_agent_cancel,
|
|
1849
1981
|
)
|
|
1850
1982
|
|
|
@@ -1869,8 +2001,17 @@ class BaseAgent(ABC):
|
|
|
1869
2001
|
# Stop keyboard listener if it was started
|
|
1870
2002
|
if key_listener_stop_event is not None:
|
|
1871
2003
|
key_listener_stop_event.set()
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2004
|
+
|
|
2005
|
+
# Unregister Windows Ctrl handler
|
|
2006
|
+
if sys.platform == "win32" and _windows_ctrl_handler is not None:
|
|
2007
|
+
try:
|
|
2008
|
+
import ctypes
|
|
2009
|
+
|
|
2010
|
+
kernel32 = ctypes.windll.kernel32
|
|
2011
|
+
kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, False)
|
|
2012
|
+
except Exception:
|
|
2013
|
+
pass # Best effort cleanup
|
|
2014
|
+
|
|
2015
|
+
# Restore original signal handler (Unix)
|
|
2016
|
+
if original_handler is not None:
|
|
1876
2017
|
signal.signal(signal.SIGINT, original_handler)
|
|
@@ -790,5 +790,6 @@ def main_entry():
|
|
|
790
790
|
DBOS.destroy()
|
|
791
791
|
return 0
|
|
792
792
|
finally:
|
|
793
|
-
# Reset terminal on
|
|
793
|
+
# Reset terminal on all platforms for clean state
|
|
794
|
+
reset_windows_terminal_full() # Safe no-op on non-Windows
|
|
794
795
|
reset_unix_terminal()
|
|
@@ -86,9 +86,15 @@ def cancel_agent_uses_signal() -> bool:
|
|
|
86
86
|
"""Check if the cancel agent key uses SIGINT (Ctrl+C).
|
|
87
87
|
|
|
88
88
|
Returns:
|
|
89
|
-
True if the cancel key is ctrl+c
|
|
90
|
-
False if it uses keyboard listener approach.
|
|
89
|
+
True if the cancel key is ctrl+c AND we're not on Windows
|
|
90
|
+
(uses SIGINT handler), False if it uses keyboard listener approach.
|
|
91
91
|
"""
|
|
92
|
+
import sys
|
|
93
|
+
|
|
94
|
+
# On Windows, always use keyboard listener - SIGINT is unreliable
|
|
95
|
+
if sys.platform == "win32":
|
|
96
|
+
return False
|
|
97
|
+
|
|
92
98
|
return get_cancel_agent_key() == "ctrl+c"
|
|
93
99
|
|
|
94
100
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shell command safety assessment agent.
|
|
2
|
+
|
|
3
|
+
This agent provides rapid risk assessment of shell commands before execution.
|
|
4
|
+
It's designed to be ultra-lightweight with a concise prompt (<200 tokens) and
|
|
5
|
+
uses structured output for reliable parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, List
|
|
9
|
+
|
|
10
|
+
from code_puppy.agents.base_agent import BaseAgent
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShellSafetyAgent(BaseAgent):
|
|
17
|
+
"""Lightweight agent for assessing shell command safety risks.
|
|
18
|
+
|
|
19
|
+
This agent evaluates shell commands for potential risks including:
|
|
20
|
+
- File system destruction (rm -rf, dd, format, mkfs)
|
|
21
|
+
- Database operations (DROP, TRUNCATE, unfiltered UPDATE/DELETE)
|
|
22
|
+
- Privilege escalation (sudo, su, chmod 777)
|
|
23
|
+
- Network operations (wget/curl to unknown hosts)
|
|
24
|
+
- Data exfiltration patterns
|
|
25
|
+
|
|
26
|
+
The agent returns structured output with a risk level and brief reasoning.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
"""Agent name for internal use."""
|
|
32
|
+
return "shell_safety_checker"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def display_name(self) -> str:
|
|
36
|
+
"""User-facing display name."""
|
|
37
|
+
return "Shell Safety Checker 🛡️"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self) -> str:
|
|
41
|
+
"""Agent description."""
|
|
42
|
+
return "Lightweight agent that assesses shell command safety risks"
|
|
43
|
+
|
|
44
|
+
def get_system_prompt(self) -> str:
|
|
45
|
+
"""Get the ultra-concise system prompt for shell safety assessment.
|
|
46
|
+
|
|
47
|
+
This prompt is kept under 200 tokens for fast inference and low cost.
|
|
48
|
+
"""
|
|
49
|
+
return """You are a shell command safety analyzer. Assess risk levels concisely.
|
|
50
|
+
|
|
51
|
+
**Risk Levels:**
|
|
52
|
+
- none: Completely safe (ls, pwd, echo, cat readonly files)
|
|
53
|
+
- low: Minimal risk (mkdir, touch, git status, read-only queries)
|
|
54
|
+
- medium: Moderate risk (file edits, package installs, service restarts)
|
|
55
|
+
- high: Significant risk (rm files, UPDATE/DELETE without WHERE, TRUNCATE, chmod dangerous permissions)
|
|
56
|
+
- critical: Severe/destructive (rm -rf, DROP TABLE/DATABASE, dd, format, mkfs, bq delete dataset, unfiltered mass deletes)
|
|
57
|
+
|
|
58
|
+
**Evaluate:**
|
|
59
|
+
- Scope (single file vs. entire system)
|
|
60
|
+
- Reversibility (can it be undone?)
|
|
61
|
+
- Data loss potential
|
|
62
|
+
- Privilege requirements
|
|
63
|
+
- Database destruction patterns
|
|
64
|
+
|
|
65
|
+
**Output:** Risk level + reasoning (max 1 sentence)."""
|
|
66
|
+
|
|
67
|
+
def get_available_tools(self) -> List[str]:
|
|
68
|
+
"""This agent uses no tools - pure reasoning only."""
|
|
69
|
+
return []
|
{code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/register_callbacks.py
RENAMED
|
@@ -7,12 +7,42 @@ and assesses their safety risk before execution.
|
|
|
7
7
|
from typing import Any, Dict, Optional
|
|
8
8
|
|
|
9
9
|
from code_puppy.callbacks import register_callback
|
|
10
|
-
from code_puppy.config import
|
|
10
|
+
from code_puppy.config import (
|
|
11
|
+
get_global_model_name,
|
|
12
|
+
get_safety_permission_level,
|
|
13
|
+
get_yolo_mode,
|
|
14
|
+
)
|
|
11
15
|
from code_puppy.messaging import emit_info
|
|
12
16
|
from code_puppy.plugins.shell_safety.command_cache import (
|
|
13
17
|
cache_assessment,
|
|
14
18
|
get_cached_assessment,
|
|
15
19
|
)
|
|
20
|
+
from code_puppy.tools.command_runner import ShellSafetyAssessment
|
|
21
|
+
|
|
22
|
+
# OAuth model prefixes - these models have their own safety mechanisms
|
|
23
|
+
OAUTH_MODEL_PREFIXES = (
|
|
24
|
+
"claude-code-", # Anthropic OAuth
|
|
25
|
+
"chatgpt-", # OpenAI OAuth
|
|
26
|
+
"gemini-oauth", # Google OAuth
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_oauth_model(model_name: str | None) -> bool:
|
|
31
|
+
"""Check if the model is an OAuth model that should skip safety checks.
|
|
32
|
+
|
|
33
|
+
OAuth models have their own built-in safety mechanisms, so we skip
|
|
34
|
+
the shell safety callback to avoid redundant checks and potential bugs.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
model_name: The name of the current model
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if the model is an OAuth model, False otherwise
|
|
41
|
+
"""
|
|
42
|
+
if not model_name:
|
|
43
|
+
return False
|
|
44
|
+
return model_name.startswith(OAUTH_MODEL_PREFIXES)
|
|
45
|
+
|
|
16
46
|
|
|
17
47
|
# Risk level hierarchy for numeric comparison
|
|
18
48
|
# Lower numbers = safer commands, higher numbers = more dangerous
|
|
@@ -68,6 +98,11 @@ async def shell_safety_callback(
|
|
|
68
98
|
None if command is safe to proceed
|
|
69
99
|
Dict with rejection info if command should be blocked
|
|
70
100
|
"""
|
|
101
|
+
# Skip safety checks for OAuth models - they have their own safety mechanisms
|
|
102
|
+
current_model = get_global_model_name()
|
|
103
|
+
if is_oauth_model(current_model):
|
|
104
|
+
return None
|
|
105
|
+
|
|
71
106
|
# Only check safety in yolo_mode - otherwise user is reviewing manually
|
|
72
107
|
yolo_mode = get_yolo_mode()
|
|
73
108
|
if not yolo_mode:
|
|
@@ -108,8 +143,14 @@ async def shell_safety_callback(
|
|
|
108
143
|
# Create agent and assess command
|
|
109
144
|
agent = ShellSafetyAgent()
|
|
110
145
|
|
|
111
|
-
#
|
|
112
|
-
|
|
146
|
+
# Build the assessment prompt with optional cwd context
|
|
147
|
+
prompt = f"Assess this shell command:\n\nCommand: {command}"
|
|
148
|
+
if cwd:
|
|
149
|
+
prompt += f"\nWorking directory: {cwd}"
|
|
150
|
+
|
|
151
|
+
# Run async assessment with structured output type
|
|
152
|
+
result = await agent.run_with_mcp(prompt, output_type=ShellSafetyAssessment)
|
|
153
|
+
assessment = result.output
|
|
113
154
|
|
|
114
155
|
# Cache the result for future use, but only if it's not a fallback assessment
|
|
115
156
|
if not getattr(assessment, "is_fallback", False):
|
|
@@ -192,6 +192,11 @@ def kill_all_running_shell_processes() -> int:
|
|
|
192
192
|
"""Kill all currently tracked running shell processes and stop reader threads.
|
|
193
193
|
|
|
194
194
|
Returns the number of processes signaled.
|
|
195
|
+
|
|
196
|
+
Implementation notes:
|
|
197
|
+
- Atomically snapshot and clear the registry to prevent race conditions
|
|
198
|
+
- Deduplicate by PID to ensure each process is killed at most once
|
|
199
|
+
- Let exceptions from _kill_process_group propagate (tests expect this)
|
|
195
200
|
"""
|
|
196
201
|
global _READER_STOP_EVENT
|
|
197
202
|
|
|
@@ -199,30 +204,52 @@ def kill_all_running_shell_processes() -> int:
|
|
|
199
204
|
if _READER_STOP_EVENT:
|
|
200
205
|
_READER_STOP_EVENT.set()
|
|
201
206
|
|
|
202
|
-
|
|
207
|
+
# Atomically take snapshot and clear registry
|
|
208
|
+
# This prevents other threads from seeing/processing the same processes
|
|
203
209
|
with _RUNNING_PROCESSES_LOCK:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
procs_snapshot = list(_RUNNING_PROCESSES)
|
|
211
|
+
_RUNNING_PROCESSES.clear()
|
|
212
|
+
|
|
213
|
+
# Deduplicate by pid to ensure at-most-one kill per process
|
|
214
|
+
seen_pids: set = set()
|
|
215
|
+
killed_count = 0
|
|
216
|
+
|
|
217
|
+
for proc in procs_snapshot:
|
|
218
|
+
if proc is None:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
pid = getattr(proc, "pid", None)
|
|
222
|
+
key = pid if pid is not None else id(proc)
|
|
223
|
+
|
|
224
|
+
if key in seen_pids:
|
|
225
|
+
continue
|
|
226
|
+
seen_pids.add(key)
|
|
227
|
+
|
|
228
|
+
# Close pipes first to unblock readline()
|
|
207
229
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
if proc.stdout and not proc.stdout.closed:
|
|
231
|
+
proc.stdout.close()
|
|
232
|
+
if proc.stderr and not proc.stderr.closed:
|
|
233
|
+
proc.stderr.close()
|
|
234
|
+
if proc.stdin and not proc.stdin.closed:
|
|
235
|
+
proc.stdin.close()
|
|
236
|
+
except (OSError, ValueError):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
# Only attempt to kill processes that are still running
|
|
240
|
+
if proc.poll() is None:
|
|
241
|
+
# Let exceptions bubble up (tests expect this behavior)
|
|
242
|
+
_kill_process_group(proc)
|
|
243
|
+
killed_count += 1
|
|
244
|
+
|
|
245
|
+
# Track user-killed PIDs
|
|
246
|
+
if pid is not None:
|
|
247
|
+
try:
|
|
248
|
+
_USER_KILLED_PROCESSES.add(pid)
|
|
249
|
+
except Exception:
|
|
250
|
+
pass # Non-fatal bookkeeping
|
|
218
251
|
|
|
219
|
-
|
|
220
|
-
_kill_process_group(p)
|
|
221
|
-
count += 1
|
|
222
|
-
_USER_KILLED_PROCESSES.add(p.pid)
|
|
223
|
-
finally:
|
|
224
|
-
_unregister_process(p)
|
|
225
|
-
return count
|
|
252
|
+
return killed_count
|
|
226
253
|
|
|
227
254
|
|
|
228
255
|
def get_running_shell_process_count() -> int:
|