Skip to content

Commit 6cc0d18

Browse files
committed
Correcting the last features.
Adding approval on tools. Modifying the context and system prompts of the tools. Adding approvals on agents depending on the mode. Modifying the CLI chat interface. Big changes to the whole thing.
1 parent a8772ed commit 6cc0d18

25 files changed

Lines changed: 1545 additions & 336 deletions

deadend_cli/cli/chat.py

Lines changed: 149 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
import sys
1616
from enum import Enum
1717
from typing import Dict, List, Callable, Optional
18-
import logfire
1918
from rich.console import Console
2019
from rich.layout import Layout
2120
from rich.panel import Panel
2221
from rich.box import ROUNDED
2322
from prompt_toolkit.application import Application
24-
from prompt_toolkit.widgets import TextArea, Frame, Label
23+
from prompt_toolkit.widgets import TextArea, Frame, Label, RadioList
2524
from prompt_toolkit.layout import Layout as PTKLayout
2625
from prompt_toolkit.key_binding import KeyBindings
2726
from prompt_toolkit.styles import Style
@@ -38,6 +37,7 @@
3837
from deadend_cli.core.agents.judge import JudgeOutput
3938
from deadend_cli.core.utils.network import check_target_alive
4039
from deadend_cli.core.models import ModelRegistry
40+
from pydantic_ai import DeferredToolRequests
4141
from .console import console_printer
4242

4343
# Defining Agent modes
@@ -60,13 +60,8 @@ def print_pydantic_model(obj: BaseModel, title: str = "Agent Output") -> None:
6060

6161
# Add each field to the table
6262
for field_name, field_value in obj.model_dump().items():
63-
# Format the value for display
64-
if isinstance(field_value, str) and len(field_value) > 100:
65-
# Truncate long strings
66-
display_value = field_value[:100] + "..." if len(field_value) > 100 else field_value
67-
else:
68-
display_value = str(field_value)
69-
63+
# Display the full value without truncation
64+
display_value = str(field_value)
7065
table.add_row(field_name, display_value)
7166

7267
# Create a panel with the table
@@ -187,6 +182,77 @@ def startup(self):
187182
self.console.print("[bold green] Starting Agent Mode: Chat interface.[/bold green]")
188183
self.console.print("Type '/help' for commands, '/quit' to exit.")
189184

185+
async def ask_for_approval_panel(self, title: str = "Tool Approval Required") -> Optional[str]:
186+
"""Prompt for tool execution approval using Prompt Toolkit with choices.
187+
188+
Args:
189+
title: Title text to display with the confirmation dialog.
190+
191+
Returns:
192+
'yes' for approval, 'no' for rejection, or None if cancelled
193+
"""
194+
try:
195+
style = Style.from_dict({
196+
"frame.border": "ansiyellow",
197+
"radio-checked": "ansigreen",
198+
"radio-unchecked": "ansired",
199+
})
200+
201+
# Define approval choices
202+
choices = [
203+
("yes", "Approve tool execution"),
204+
("no", "Deny tool execution"),
205+
]
206+
207+
# Create radio list for choices, default to yes
208+
radio_list = RadioList(choices, default="yes")
209+
# Set default selection to "yes"
210+
radio_list.current_value = "yes"
211+
212+
# Create confirmation prompt text
213+
prompt_text = Label(text="Do you approve these tool executions?", style="ansiwhite")
214+
215+
# Create footer with instructions
216+
footer_text = "Commands: ↑/↓=Navigate | Space=Toggle | Enter=Submit | Ctrl+C=Cancel"
217+
footer = Label(text=footer_text, style="ansiblack")
218+
219+
root_container = HSplit([
220+
Label(text=title, style="bold ansiyellow"),
221+
prompt_text,
222+
Frame(body=radio_list),
223+
footer,
224+
])
225+
226+
kb = KeyBindings()
227+
228+
@kb.add("c-c")
229+
def _(event): # type: ignore[no-redef]
230+
event.app.exit(result=None) # Cancel without selection
231+
232+
# Override RadioList's Enter behavior to exit with result
233+
def custom_enter_handler(event):
234+
radio_list._handle_enter()
235+
selected_value = radio_list.current_value
236+
event.app.exit(result=selected_value)
237+
238+
# Replace RadioList's enter binding
239+
radio_list.control.key_bindings.add("enter")(custom_enter_handler)
240+
241+
app = Application(
242+
layout=PTKLayout(container=root_container),
243+
key_bindings=kb,
244+
full_screen=False,
245+
mouse_support=False,
246+
style=style,
247+
min_redraw_interval=0.01,
248+
)
249+
app.layout.focus(radio_list)
250+
251+
return await app.run_async()
252+
except KeyboardInterrupt:
253+
console_printer.print("\n[yellow]Approval cancelled.[/yellow]")
254+
return None
255+
190256
async def ask_with_ptk_panel(
191257
self,
192258
title: str = "Prompt",
@@ -222,7 +288,7 @@ async def ask_with_ptk_panel(
222288
# Create footer with available commands
223289
footer_text = "Commands: Ctrl+C=Exit | Ctrl+I=Interrupt | Enter=Submit | /help=Help | /clear=Clear | /new-target=New Target"
224290
footer = Label(text=footer_text, style="ansiblack")
225-
291+
226292
root_container = HSplit([
227293
Label(text=title, style="bold ansicyan"),
228294
Frame(body=input_field),
@@ -236,17 +302,17 @@ def _accept_handler(_buff):
236302
input_field.accept_handler = _accept_handler
237303

238304
@kb.add("c-c")
239-
def _(event): # type: ignore[no-redef]
305+
def _(event):
240306
event.app.exit(result=None)
241307

242308
@kb.add("c-i")
243-
def _(event): # type: ignore[no-redef]
309+
def _(event):
244310
if interrupt_callback:
245311
interrupt_callback()
246312
event.app.exit(result="__INTERRUPT__")
247313

248314
@kb.add("enter")
249-
def _(event): # type: ignore[no-redef]
315+
def _(event):
250316
text = input_field.text
251317
if text.startswith("/"):
252318
# Handle commands
@@ -311,7 +377,7 @@ async def chat_interface(
311377
# Settings up sandbox
312378
try:
313379
sandbox_manager = sandbox_setup()
314-
sandbox_id = sandbox_manager.create_sandbox("kali_deadend")
380+
sandbox_id = sandbox_manager.create_sandbox("xoxruns/sandboxed_kali", network_name="host")
315381
sandbox = sandbox_manager.get_sandbox(sandbox_id=sandbox_id)
316382
except Exception as e:
317383
console_printer.print(f"[yellow]Sandbox manager could not be started : {e}. Continuing without sandbox.[/yellow]")
@@ -327,14 +393,19 @@ async def chat_interface(
327393
code_indexer_db=rag_db,
328394
sandbox=sandbox
329395
)
396+
397+
# Set up approval callback to use Prompt Toolkit
398+
async def approval_callback():
399+
return await chat_interface.ask_for_approval_panel("Tool Execution Approval Required")
400+
401+
workflow_agent.set_approval_callback(approval_callback)
330402
# Setup available agents
331403
available_agents = {
332404
'webapp_recon': "Expert cybersecurity agent that enumerates a web target to understand the architecture and understand the endpoints and where an attack vector could be tested.",
333405
# 'planner_agent': 'Expert cybersecurity agent that plans what is the next step to do',
334406
'router_agent': 'Router agent, expert that routes to the specific agent needed to achieve the next step of the plan.'
335407
}
336408
workflow_agent.register_agents(available_agents)
337-
workflow_agent.register_sandbox_runner()
338409
# Check if the provided target is reachable before proceeding
339410
alive = False
340411
while not alive:
@@ -486,79 +557,75 @@ def interrupt_agent():
486557
if not user_prompt:
487558
break
488559

489-
# Reset interruption flag
560+
# Reset interruption flag and workflow state for new execution
490561
agent_interrupted = False
491-
492-
# Reset workflow state for new execution
493562
workflow_agent.reset_workflow_state()
494563

495-
# Handle workflow execution with yielded messages
496564
judge_output = None
497-
498-
# Show running status
499-
with console_printer.status("[bold blue]Agent workflow running...", spinner="dots2"):
500-
try:
501-
async for item in workflow_agent.start_workflow(
502-
prompt=user_prompt,
503-
target=target,
504-
validation_type=None,
505-
validation_format=None
506-
):
507-
# Check if this is the final result (JudgeOutput)
508-
if isinstance(item, JudgeOutput):
509-
judge_output = item
510-
511-
# Check if this is a Pydantic BaseModel object
512-
if isinstance(item, BaseModel):
513-
# Special handling for RouterOutput - print as simple text
514-
if type(item).__name__ == "RouterOutput":
515-
console_printer.print(f"[cyan]Router:[/cyan] {item.next_agent_name}")
516-
console_printer.print(f"[cyan]Reasoning:[/cyan] {item.reasoning}")
517-
else:
518-
# Determine the type of model for better title
519-
model_type = type(item).__name__
520-
print_pydantic_model(item, f"{model_type} Output")
521-
# Check if this is a list of tasks (from PlannerOutput)
522-
elif isinstance(item, list) and len(item) > 0 and hasattr(item[0], 'goal'):
523-
# Create a table for tasks
524-
task_table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
525-
task_table.add_column("Step", style="cyan", no_wrap=True)
526-
task_table.add_column("Goal", style="white")
527-
task_table.add_column("Status", style="yellow")
528-
task_table.add_column("Output", style="green")
529-
530-
for i, task in enumerate(item, 1):
531-
task_table.add_row(
532-
str(i),
533-
task.goal,
534-
task.status,
535-
task.output
536-
)
537-
538-
task_panel = Panel(
539-
task_table,
540-
title="[bold green]Planned Tasks[/bold green]",
541-
border_style="green",
542-
box=box.ROUNDED
543-
)
544-
console_printer.print(task_panel)
565+
try:
566+
async for item in workflow_agent.start_workflow(
567+
prompt=user_prompt,
568+
target=target,
569+
validation_type=None,
570+
validation_format=None
571+
):
572+
# Skip printing DeferredToolRequests objects
573+
if hasattr(item, 'output') and isinstance(item.output, DeferredToolRequests):
574+
continue
575+
576+
# Special handling for RequesterOutput - print just the reasoning
577+
if isinstance(item, RequesterOutput):
578+
console_printer.print(f"[bold green]Requester Analysis:[/bold green] {item.reasoning}")
579+
continue
580+
581+
# Check if this is the final result (JudgeOutput)
582+
if isinstance(item, JudgeOutput):
583+
judge_output = item
584+
585+
# Check if this is a Pydantic BaseModel object
586+
if isinstance(item, BaseModel):
587+
# Special handling for RouterOutput - print as simple text
588+
if type(item).__name__ == "RouterOutput":
589+
console_printer.print(f"[cyan]Router:[/cyan] \
590+
{item.next_agent_name}")
591+
console_printer.print(f"[cyan]Reasoning:[/cyan] {item.reasoning}")
545592
else:
546-
# Print regular string messages
547-
console_printer.print(item)
548-
549-
# Check for interruption
550-
if agent_interrupted or workflow_agent.interrupted:
551-
break
593+
# Determine the type of model for better title
594+
model_type = type(item).__name__
595+
print_pydantic_model(item, f"{model_type} Output")
596+
597+
elif isinstance(item, list) and len(item) > 0 and hasattr(item[0], 'goal'):
598+
# Create a simple text display for tasks without truncation
599+
tasks_text = "[bold green]Planned Tasks[/bold green]\n\n"
600+
for i, task in enumerate(item, 1):
601+
tasks_text += f"[cyan]Step {i}:[/cyan]\n"
602+
tasks_text += f"[white]Goal:[/white] {task.goal}\n"
603+
tasks_text += f"[yellow]Status:[/yellow] {task.status}\n"
604+
tasks_text += f"[green]Output:[/green]\n{task.output}\n\n"
605+
606+
task_panel = Panel(
607+
tasks_text.strip(),
608+
title="Tasks Overview",
609+
border_style="green",
610+
box=box.ROUNDED
611+
)
612+
console_printer.print(task_panel)
613+
else:
614+
# Print regular string messages
615+
console_printer.print(item)
552616

553-
# Small delay to allow for interruption
554-
await asyncio.sleep(0.1)
617+
# Check for interruption
618+
if agent_interrupted or workflow_agent.interrupted:
619+
break
555620

556-
except InterruptedError as e:
557-
console_printer.print(f"[yellow]Workflow interrupted: {e}[/yellow]")
558-
judge_output = None
559-
except Exception as e:
560-
console_printer.print(f"[red]Workflow error: {e}[/red]")
561-
judge_output = None
621+
# Small delay to allow for interruption
622+
await asyncio.sleep(0.1)
623+
except InterruptedError as e:
624+
console_printer.print(f"[yellow]Workflow interrupted: {e}[/yellow]")
625+
judge_output = None
626+
except Exception as e:
627+
console_printer.print(f"[red]Workflow error: {e}[/red]")
628+
judge_output = None
562629

563630
# Check if agent was interrupted
564631
if agent_interrupted or workflow_agent.interrupted:
@@ -585,8 +652,7 @@ def interrupt_agent():
585652
console_printer.print("\n[bold cyan]Solution:[/bold cyan]")
586653
console_printer.print(f"{judge_output.output.solution}")
587654

588-
# Reset user prompt to ask for a new one in the next iteration
589-
user_prompt = None
655+
user_prompt = None
590656
except KeyboardInterrupt:
591657
console_printer.print("\n[yellow]Received Ctrl+C. Exiting gracefully...[/yellow]")
592658
console_printer.print("[green]Thank you for using Deadend CLI![/green]")

deadend_cli/cli/cli.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import typer
1313
import docker
1414
from rich.console import Console
15+
import logfire
1516

1617
from deadend_cli.core import config_setup
1718
from deadend_cli.cli.chat import chat_interface, Modes
1819
from deadend_cli.cli.eval import eval_interface
1920
from deadend_cli.cli.banner import print_banner
20-
from deadend_cli.cli.init import init_cli_config, check_docker, check_pgvector_container, stop_pgvector_container
21+
from deadend_cli.cli.init import init_cli_config, check_docker, \
22+
check_pgvector_container, stop_pgvector_container, setup_pgvector_database
2123

2224
console = Console()
2325

@@ -51,16 +53,21 @@ def chat(
5153
console.print("Please install Docker from: https://docs.docker.com/get-docker/")
5254
console.print("Make sure Docker daemon is running, then run this command again.")
5355
raise typer.Exit(1)
54-
55-
# Check pgvector database
56+
57+
# Check pgvector database and setup if not running
5658
if not check_pgvector_container(docker_client):
57-
console.print("\n[red]pgvector database is not running.[/red]")
58-
console.print("Please run 'deadend-cli init' to set up the required services.")
59-
raise typer.Exit(1)
59+
console.print("\n[blue]pgvector database is not running. Setting up...[/blue]")
60+
if not setup_pgvector_database(docker_client):
61+
console.print("\n[red]Failed to setup pgvector database.[/red]")
62+
console.print("Please check Docker logs and try again.")
63+
raise typer.Exit(1)
6064

6165
# Init configuration
6266
config = config_setup()
6367
print_banner(config=config)
68+
# Monitoring
69+
# logfire.configure(scrubbing=False)
70+
# logfire.instrument_pydantic_ai()
6471

6572
try:
6673
asyncio.run(

0 commit comments

Comments
 (0)