Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code_puppy/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def __init__(self):
# This is populated after the first successful run when MCP tools are retrieved
self._mcp_tool_definitions_cache: List[Dict[str, Any]] = []

@property
def code_generation_agent(self):
"""Public accessor for the underlying pydantic-ai Agent."""
return self._code_generation_agent

def get_identity(self) -> str:
"""Get a unique identity for this agent instance.
Expand Down
66 changes: 66 additions & 0 deletions code_puppy/cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,69 @@ async def main():
parser.add_argument(
"command", nargs="*", help="Run a single command (deprecated, use -p instead)"
)
parser.add_argument(
"--acp",
action="store_true",
help="Start ACP Gateway (Agent Communication Protocol) server",
)
parser.add_argument(
"--acp-transport",
choices=["http", "stdio"],
default="http",
help="ACP transport: http (port 9001) or stdio (stdin/stdout). Default: http",
)
args = parser.parse_args()

# ACP stdio mode: early exit — skip banner, version check, renderers.
# stdio needs clean stdout (only JSON-RPC), all logs go to stderr.
if args.acp and args.acp_transport == 'stdio':
import logging
os.environ["ACP_ENABLED"] = "true"
os.environ["ACP_TRANSPORT"] = "stdio"
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)

# Initialize DBOS — pydantic-ai's .run() goes through DBOS
# durable execution and will raise DBOSException if not launched.
if get_use_dbos():
dbos_app_version = os.environ.get(
"DBOS_APP_VERSION",
f"{__version__}-{int(time.time() * 1000)}",
)
dbos_config: DBOSConfig = {
"name": "dbos-code-puppy",
"system_database_url": DBOS_DATABASE_URL,
"run_admin_server": False,
"conductor_key": os.environ.get("DBOS_CONDUCTOR_KEY"),
"log_level": os.environ.get("DBOS_LOG_LEVEL", "ERROR"),
"application_version": dbos_app_version,
}
try:
DBOS(config=dbos_config)
DBOS.launch()
except Exception as e:
logging.getLogger(__name__).error("DBOS init failed: %s", e)
return

# Plugins already loaded at module level — no need to reload here.
# Do NOT call callbacks.on_startup() because that would start a
# SECOND stdio server in a background thread.
try:
from code_puppy.plugins.acp_gateway.agent import run_code_puppy_agent
await run_code_puppy_agent()
except KeyboardInterrupt:
pass
finally:
if get_use_dbos():
try:
DBOS.destroy()
except Exception:
pass
return

from code_puppy.messaging import (
RichConsoleRenderer,
SynchronousInteractiveRenderer,
Expand Down Expand Up @@ -320,6 +381,11 @@ def _uvx_protective_sigint_handler(_sig, _frame):
initial_command = None
prompt_only_mode = False

if args.acp:
os.environ["ACP_ENABLED"] = "true"
os.environ["ACP_TRANSPORT"] = args.acp_transport
# http mode: falls through to interactive mode with ACP enabled in background
Comment on lines +384 to +387
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check the register_callbacks._on_startup implementation
rg -n "on_startup|_on_startup|ACP_ENABLED|from_env" code_puppy/plugins/acp_gateway/register_callbacks.py -A 5

# Also check ACPConfig usage throughout the ACP gateway
rg -n "from_env|ACPConfig" code_puppy/plugins/acp_gateway/ -A 3

Repository: mpfaffenberger/code_puppy

Length of output: 1677


🏁 Script executed:

# Read the relevant section of cli_runner.py
sed -n '310,365p' code_puppy/cli_runner.py

Repository: mpfaffenberger/code_puppy

Length of output: 2104


Move ACP env var setup before on_startup() callback.

Lines 355-358 set ACP_ENABLED and ACP_TRANSPORT, but this occurs after await callbacks.on_startup() at line 320. The startup callback triggers _on_startup() in register_callbacks.py, which immediately calls ACPConfig.from_env() (line 52) to read these env vars—before they are set. The env vars must be set before line 320 for the HTTP transport startup path to initialize correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/cli_runner.py` around lines 355 - 358, Move the ACP environment
variable setup so it occurs before the startup callbacks are invoked: set
os.environ["ACP_ENABLED"] and os.environ["ACP_TRANSPORT"] when args.acp is true
earlier in the flow, before the call to await callbacks.on_startup() (which
triggers register_callbacks._on_startup() that calls ACPConfig.from_env()).
Ensure the args.acp branch that sets ACP_ENABLED/ACP_TRANSPORT is executed prior
to invoking on_startup()/callbacks.on_startup() so ACPConfig.from_env() reads
the correct values.


if args.prompt:
initial_command = args.prompt
prompt_only_mode = True
Expand Down
22 changes: 22 additions & 0 deletions code_puppy/plugins/acp_gateway/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""ACP Gateway Plugin.

Exposes Code Puppy as an ACP (Agent Client Protocol) agent using the
official ``agent-client-protocol`` Python SDK.

The SDK handles all transport concerns (stdio JSON-RPC, session
lifecycle, content blocks). This plugin provides the bridge between
ACP and Code Puppy's pydantic-ai agent system.

The plugin gracefully degrades — if ``agent-client-protocol`` is not
installed, Code Puppy starts normally with a warning log.
"""

__version__ = "0.2.0"
__description__ = "ACP Gateway plugin for Code Puppy"

from code_puppy.plugins.acp_gateway.agent import CodePuppyAgent, run_code_puppy_agent

__all__ = [
"CodePuppyAgent",
"run_code_puppy_agent",
]
Comment on lines +17 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the structure and see if the files exist
git ls-files code_puppy/plugins/acp_gateway/

# Check agent.py for unconditional acp imports
rg -n "from acp import\|import acp" code_puppy/plugins/acp_gateway/agent.py

# Check the __init__.py content
cat -n code_puppy/plugins/acp_gateway/__init__.py

# Check if there's any graceful degradation pattern elsewhere in the plugin
rg -n "gracefully degrade\|optional\|ImportError" code_puppy/plugins/acp_gateway/

Repository: mpfaffenberger/code_puppy

Length of output: 1172


🏁 Script executed:

# Read agent.py to check its imports
cat -n code_puppy/plugins/acp_gateway/agent.py | head -40

# Also check if there's any conditional import guard mechanism in place
rg -B5 -A5 "CodePuppyAgent\|run_code_puppy_agent" code_puppy/plugins/acp_gateway/agent.py | head -50

Repository: mpfaffenberger/code_puppy

Length of output: 1391


🏁 Script executed:

# Check register_callbacks.py to see how this plugin is loaded/registered
cat -n code_puppy/plugins/acp_gateway/register_callbacks.py

# Also check if there's any error handling at the plugin loading level
rg -n "try:|except" code_puppy/plugins/acp_gateway/__init__.py code_puppy/plugins/acp_gateway/register_callbacks.py

Repository: mpfaffenberger/code_puppy

Length of output: 4153


🏁 Script executed:

# Check how the plugin is imported/loaded in the core system
rg -n "from code_puppy.plugins.acp_gateway import\|import code_puppy.plugins.acp_gateway" --type py

# Check the plugin loading mechanism
fd "plugin" code_puppy/ --type f -e py | head -20
rg -n "plugins\|register_callback" code_puppy/ -t py | grep -i "import\|load" | head -20

Repository: mpfaffenberger/code_puppy

Length of output: 220


🏁 Script executed:

# Check how plugins are discovered and loaded
rg -n "plugins" code_puppy/ -t py --max-count 5 | head -30

# Look at the main entry point or plugin loader
fd -name "*.py" code_puppy/ -type f | xargs grep -l "register_callback\|plugins" | head -10

# Check if register_callbacks.py is auto-discovered
cat -n code_puppy/__init__.py code_puppy/core.py 2>/dev/null | head -50

Repository: mpfaffenberger/code_puppy

Length of output: 4133


🏁 Script executed:

# Look for the plugin loading mechanism
fd "callbacks.py" code_puppy/ -type f

# Check the callbacks module
cat -n code_puppy/callbacks.py | head -80

# Look for where plugins are discovered/loaded
rg -n "discover\|load.*plugin\|import.*plugin" code_puppy/ -t py | grep -v "test" | head -20

Repository: mpfaffenberger/code_puppy

Length of output: 2860


🏁 Script executed:

# Search for where plugins are imported/discovered at startup
rg -n "plugins" code_puppy/ --glob "*.py" | grep -i "import\|load\|discover" | head -30

# Check if there's a plugin discovery mechanism
fd "register_callbacks.py" code_puppy/plugins/

# Look at the main entry point
cat -n code_puppy/command_line/app.py | head -100

Repository: mpfaffenberger/code_puppy

Length of output: 4316


🏁 Script executed:

# Find the main entry point
fd "main.py\|__main__.py\|app.py" code_puppy/ --type f

# Check for plugin auto-discovery
rg -n "pkgutil\|importlib\|namespace\|entry_points" code_puppy/ --glob "*.py" | head -20

# Look at a known plugin's register_callbacks.py to see the pattern
cat -n code_puppy/plugins/claude_code_oauth/register_callbacks.py | head -30

Repository: mpfaffenberger/code_puppy

Length of output: 1223


🏁 Script executed:

# Search for where register_callbacks modules are imported at startup
rg -n "register_callbacks" code_puppy/ --glob "*.py" | grep -v "plugins/" | head -20

# Check if there's a pattern where __init__.py is imported from plugins
rg -n "from code_puppy.plugins\." code_puppy/command_line/ --glob "*.py" 2>/dev/null || echo "No command_line directory"

# Find the entry point
find code_puppy -maxdepth 2 -name "__main__.py" -o -name "main.py" -o -name "cli.py" | head -5

# Check the main CLI entry point
fd "__main__" code_puppy --type f

Repository: mpfaffenberger/code_puppy

Length of output: 2001


🏁 Script executed:

# Check the main entry point
cat -n code_puppy/main.py | head -150

# Check __main__.py
cat -n code_puppy/__main__.py

Repository: mpfaffenberger/code_puppy

Length of output: 684


🏁 Script executed:

# Check cli_runner.py for plugin loading
cat -n code_puppy/cli_runner.py | head -200

Repository: mpfaffenberger/code_puppy

Length of output: 8057


🏁 Script executed:

# Check code_puppy/plugins/__init__.py to see load_plugin_callbacks
cat -n code_puppy/plugins/__init__.py

Repository: mpfaffenberger/code_puppy

Length of output: 8080


🏁 Script executed:

# Check if __init__.py is ever imported directly in the codebase
rg -n "from code_puppy.plugins.acp_gateway import\|import code_puppy.plugins.acp_gateway" code_puppy/ --glob "*.py"

# Also check the specific cli_runner import path more closely
sed -n '105,125p' code_puppy/cli_runner.py

Repository: mpfaffenberger/code_puppy

Length of output: 954


Wrap the import in __init__.py to protect the module's public API.

The docstring states the plugin "gracefully degrades" when agent-client-protocol is not installed, but line 17 has an unconditional import from agent.py, which unconditionally imports from the acp package (line 26 of agent.py). If someone imports directly from the __init__.py module (e.g., from code_puppy.plugins.acp_gateway import CodePuppyAgent), it will raise ImportError and violate the graceful degradation guarantee.

Wrap the import in a try/except so the module remains importable regardless. Per coding guidelines: "Wrap risky operations in try/except blocks in plugin code to ensure a broken plugin never crashes the entire application."

Proposed fix
+try:
+    from code_puppy.plugins.acp_gateway.agent import CodePuppyAgent, run_code_puppy_agent
+except ImportError:
+    import logging as _logging
+    _logging.getLogger(__name__).warning(
+        "acp-sdk is not installed — ACP Gateway plugin disabled. "
+        "Install with: pip install agent-client-protocol"
+    )
+    CodePuppyAgent = None  # type: ignore[assignment,misc]
+    run_code_puppy_agent = None  # type: ignore[assignment]
+
-from code_puppy.plugins.acp_gateway.agent import CodePuppyAgent, run_code_puppy_agent

 __all__ = [
     "CodePuppyAgent",
     "run_code_puppy_agent",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/plugins/acp_gateway/__init__.py` around lines 17 - 22, The import
of CodePuppyAgent and run_code_puppy_agent in __init__.py must be guarded so the
package can be imported even when the external acp dependency is missing; wrap
the from code_puppy.plugins.acp_gateway.agent import CodePuppyAgent,
run_code_puppy_agent in a try/except ImportError (or broad Exception) block, and
in the except assign safe fallbacks (e.g., set CodePuppyAgent and
run_code_puppy_agent to None or to small stub callables that raise a clear
ImportError when used) and ensure __all__ only exposes the names or remains the
same while the fallbacks provide clear errors — reference the symbols
CodePuppyAgent, run_code_puppy_agent and the source agent.py/acp import chain
when locating the code to change.

21 changes: 21 additions & 0 deletions code_puppy/plugins/acp_gateway/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Allow running the ACP agent as: python -m code_puppy.plugins.acp_gateway
All transport concerns (stdio JSON-RPC) are handled by the ACP SDK.
We just start the CodePuppyAgent and let ``run_agent()`` do the rest.
"""

import asyncio
import logging
import sys

from code_puppy.plugins.acp_gateway.agent import run_code_puppy_agent

# Redirect logging to stderr so stdout stays clean for JSON-RPC
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)

if __name__ == "__main__":
asyncio.run(run_code_puppy_agent())
Loading