From 5f5efeadbc2f6cf311a58adab5a1987719262d3f Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 19 Feb 2026 17:03:47 +0200 Subject: [PATCH 1/4] Add deckard-base-url --- packages/tui/internal/screens/redteam/catalog.go | 2 ++ .../red_team/base_red_team_attacker_agent.py | 3 +++ rogue/evaluator_agent/red_team/factory.py | 3 +++ rogue/server/core/red_team_orchestrator.py | 10 ++++------ rogue/server/qualifire_attacks.py | 1 + rogue/server/red_teaming/catalog/attacks.py | 12 ++++++++++++ rogue/server/red_teaming/catalog/vulnerabilities.py | 1 + rogue/server/red_teaming/orchestrator.py | 3 ++- rogue/server/services/red_team_service.py | 1 + sdks/python/rogue_sdk/types.py | 1 + 10 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/tui/internal/screens/redteam/catalog.go b/packages/tui/internal/screens/redteam/catalog.go index cdfa4609..f01ac442 100644 --- a/packages/tui/internal/screens/redteam/catalog.go +++ b/packages/tui/internal/screens/redteam/catalog.go @@ -161,6 +161,8 @@ var AttackCatalog = map[string]*Attack{ "permission-escalation": {ID: "permission-escalation", Name: "Permission Escalation", Category: AttackCategorySingleTurn, Description: "Attempts to bypass permission checks by claiming elevated status", Premium: true}, "system-override": {ID: "system-override", Name: "System Override", Category: AttackCategorySingleTurn, Description: "Uses explicit system override commands to bypass restrictions", Premium: true}, "semantic-manipulation": {ID: "semantic-manipulation", Name: "Semantic Manipulation", Category: AttackCategorySingleTurn, Description: "Uses semantic tricks and complex phrasing to disguise intent", Premium: true}, + // Indirect injection attacks + "html-indirect-prompt-injection": {ID: "html-indirect-prompt-injection", Name: "HTML Indirect Prompt Injection", Category: AttackCategorySingleTurn, Description: "Tricks web-browsing agents into fetching a URL that serves HTML with hidden prompt injection instructions", Premium: true}, // Advanced premium attacks "citation": {ID: "citation", Name: "Citation", Category: AttackCategorySingleTurn, Description: "Frames harmful content as academic citations or references", Premium: true}, "gcg": {ID: "gcg", Name: "GCG (Greedy Coordinate Gradient)", Category: AttackCategorySingleTurn, Description: "Gradient-based adversarial suffix generation", Premium: true}, diff --git a/rogue/evaluator_agent/red_team/base_red_team_attacker_agent.py b/rogue/evaluator_agent/red_team/base_red_team_attacker_agent.py index ba74e0c2..22c0f170 100644 --- a/rogue/evaluator_agent/red_team/base_red_team_attacker_agent.py +++ b/rogue/evaluator_agent/red_team/base_red_team_attacker_agent.py @@ -60,6 +60,7 @@ def __init__( judge_llm_api_version: Optional[str] = None, attacker_llm_api_base: Optional[str] = None, attacker_llm_api_version: Optional[str] = None, + deckard_base_url: Optional[str] = None, ): self._evaluated_agent_address = evaluated_agent_address self._protocol = protocol @@ -83,12 +84,14 @@ def __init__( self._judge_llm_api_base = judge_llm_api_base self._judge_llm_api_version = judge_llm_api_version self._qualifire_api_key = qualifire_api_key + self._deckard_base_url = deckard_base_url # Create the underlying orchestrator self._orchestrator = RedTeamOrchestrator( config=red_team_config, business_context=business_context, qualifire_api_key=qualifire_api_key, + deckard_base_url=deckard_base_url, ) # Store for easy access diff --git a/rogue/evaluator_agent/red_team/factory.py b/rogue/evaluator_agent/red_team/factory.py index 59a2f997..b80265eb 100644 --- a/rogue/evaluator_agent/red_team/factory.py +++ b/rogue/evaluator_agent/red_team/factory.py @@ -37,6 +37,7 @@ class _CommonKwargs(TypedDict): judge_llm_api_base: Optional[str] judge_llm_api_version: Optional[str] qualifire_api_key: Optional[str] + deckard_base_url: Optional[str] def create_red_team_attacker_agent( @@ -63,6 +64,7 @@ def create_red_team_attacker_agent( judge_llm_api_version: Optional[str] = None, qualifire_api_key: Optional[str] = None, python_entrypoint_file: Optional[str] = None, + deckard_base_url: Optional[str] = None, ) -> BaseRedTeamAttackerAgent: """ Create a red team attacker agent based on the protocol. @@ -142,6 +144,7 @@ def create_red_team_attacker_agent( "judge_llm_api_base": judge_llm_api_base, "judge_llm_api_version": judge_llm_api_version, "qualifire_api_key": qualifire_api_key, + "deckard_base_url": deckard_base_url, } if protocol == Protocol.A2A: diff --git a/rogue/server/core/red_team_orchestrator.py b/rogue/server/core/red_team_orchestrator.py index 7ed3cade..3cf4524b 100644 --- a/rogue/server/core/red_team_orchestrator.py +++ b/rogue/server/core/red_team_orchestrator.py @@ -7,12 +7,7 @@ from typing import Any, AsyncGenerator, Optional, Tuple -from rogue_sdk.types import ( - AuthType, - Protocol, - RedTeamConfig, - Transport, -) +from rogue_sdk.types import AuthType, Protocol, RedTeamConfig, Transport from ...common.logging import get_logger from ...evaluator_agent.red_team.factory import create_red_team_attacker_agent @@ -61,6 +56,7 @@ def __init__( attacker_llm_api_version: Optional[str] = None, business_context: str = "", python_entrypoint_file: Optional[str] = None, + deckard_base_url: Optional[str] = None, ): self.protocol = protocol self.transport = transport @@ -85,6 +81,7 @@ def __init__( self.attacker_llm_api_version = attacker_llm_api_version self.business_context = business_context self.python_entrypoint_file = python_entrypoint_file + self.deckard_base_url = deckard_base_url self.logger = get_logger(__name__) # Create attacker agent @@ -112,6 +109,7 @@ def __init__( judge_llm_api_version=judge_llm_api_version, qualifire_api_key=qualifire_api_key, python_entrypoint_file=python_entrypoint_file, + deckard_base_url=deckard_base_url, ) async def run_scan(self) -> AsyncGenerator[Tuple[str, Any], None]: diff --git a/rogue/server/qualifire_attacks.py b/rogue/server/qualifire_attacks.py index f3ca7a76..aae75396 100644 --- a/rogue/server/qualifire_attacks.py +++ b/rogue/server/qualifire_attacks.py @@ -219,6 +219,7 @@ def get_premium_attacks(self) -> List[str]: return [ # Single-turn premium + "html-indirect-prompt-injection", "homoglyph", "citation", "gcg", diff --git a/rogue/server/red_teaming/catalog/attacks.py b/rogue/server/red_teaming/catalog/attacks.py index c75adaab..fa458002 100644 --- a/rogue/server/red_teaming/catalog/attacks.py +++ b/rogue/server/red_teaming/catalog/attacks.py @@ -28,6 +28,7 @@ class AttackId(str, Enum): PROMPT_PROBING = "prompt-probing" # Premium single-turn attacks + HTML_INDIRECT_PROMPT_INJECTION = "html-indirect-prompt-injection" HEX = "hex" LEETSPEAK = "leetspeak" HOMOGLYPH = "homoglyph" @@ -192,6 +193,17 @@ class AttackId(str, Enum): description="Uses semantic tricks and complex phrasing to disguise intent", premium=True, ), + # Indirect injection attacks + AttackDef( + id=AttackId.HTML_INDIRECT_PROMPT_INJECTION, + name="HTML Indirect Prompt Injection", + category=AttackCategory.SINGLE_TURN, + description=( + "Tricks web-browsing agents into fetching a URL that serves HTML " + "with hidden prompt injection instructions" + ), + premium=True, + ), # Advanced premium attacks AttackDef( id=AttackId.CITATION, diff --git a/rogue/server/red_teaming/catalog/vulnerabilities.py b/rogue/server/red_teaming/catalog/vulnerabilities.py index eaa36f30..eaff05df 100644 --- a/rogue/server/red_teaming/catalog/vulnerabilities.py +++ b/rogue/server/red_teaming/catalog/vulnerabilities.py @@ -509,6 +509,7 @@ class VulnerabilityId(str, Enum): AttackId.PROMPT_INJECTION, AttackId.CONTEXT_POISONING, AttackId.INPUT_BYPASS, + AttackId.HTML_INDIRECT_PROMPT_INJECTION, ], ), VulnerabilityDef( diff --git a/rogue/server/red_teaming/orchestrator.py b/rogue/server/red_teaming/orchestrator.py index 818e2d5c..93a6b613 100644 --- a/rogue/server/red_teaming/orchestrator.py +++ b/rogue/server/red_teaming/orchestrator.py @@ -41,6 +41,7 @@ # Premium attacks that require the Deckard service PREMIUM_ATTACKS = { # Single-turn premium + "html-indirect-prompt-injection", "homoglyph", "citation", "gcg", @@ -128,7 +129,7 @@ def __init__( # Initialize Deckard client for premium attacks self._deckard_url = deckard_base_url or os.getenv( "DECKARD_BASE_URL", - "http://localhost:9100", + "https://deckard.rogue.security", ) self._deckard_client: Optional[DeckardClient] = None if qualifire_api_key: diff --git a/rogue/server/services/red_team_service.py b/rogue/server/services/red_team_service.py index 3561edd8..378977d2 100644 --- a/rogue/server/services/red_team_service.py +++ b/rogue/server/services/red_team_service.py @@ -136,6 +136,7 @@ async def run_job(self, job_id: str): evaluated_agent_auth_credentials=job.request.evaluated_agent_auth_credentials, # noqa: E501 red_team_config=job.request.red_team_config, qualifire_api_key=job.request.qualifire_api_key, + deckard_base_url=job.request.deckard_base_url, judge_llm=job.request.judge_llm, judge_llm_api_key=job.request.judge_llm_api_key, judge_llm_aws_access_key_id=job.request.judge_llm_aws_access_key_id, diff --git a/sdks/python/rogue_sdk/types.py b/sdks/python/rogue_sdk/types.py index 86781d4e..bdf7cb79 100644 --- a/sdks/python/rogue_sdk/types.py +++ b/sdks/python/rogue_sdk/types.py @@ -838,6 +838,7 @@ class RedTeamRequest(BaseModel): attacker_llm_api_version: Optional[str] = None business_context: str = "" qualifire_api_key: Optional[str] = None + deckard_base_url: Optional[str] = None max_retries: int = 3 timeout_seconds: int = 600 From a1d32ad5c05a3a373682669601690eceb2df174d Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 19 Feb 2026 21:56:28 +0200 Subject: [PATCH 2/4] Fix header --- rogue/server/qualifire_attacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rogue/server/qualifire_attacks.py b/rogue/server/qualifire_attacks.py index aae75396..f645b036 100644 --- a/rogue/server/qualifire_attacks.py +++ b/rogue/server/qualifire_attacks.py @@ -80,7 +80,7 @@ def _get_headers(self) -> Dict[str, str]: "User-Agent": "rogue-red-teaming/1.0", } if self.api_key: - headers["X-API-Key"] = self.api_key + headers["X-Qualifire-API-Key"] = self.api_key return headers async def generate_attack_payload( From 6da96dc826c2cb244bbd9c65d7f21fb1a148f08c Mon Sep 17 00:00:00 2001 From: Yuval Date: Fri, 20 Feb 2026 12:52:12 +0200 Subject: [PATCH 3/4] Add evaluation agent auth support in tui --- .../internal/screens/evaluations/form_view.go | 54 ++++++-- .../tui/internal/screens/evaluations/types.go | 19 ++- .../tui/internal/tui/common_controller.go | 18 ++- packages/tui/internal/tui/eval_form_view.go | 2 + packages/tui/internal/tui/eval_types.go | 78 +++++++++-- packages/tui/internal/tui/form_controller.go | 125 +++++++++++++++++- .../tui/internal/tui/keyboard_controller.go | 23 +++- packages/tui/internal/tui/utils.go | 10 +- 8 files changed, 285 insertions(+), 44 deletions(-) diff --git a/packages/tui/internal/screens/evaluations/form_view.go b/packages/tui/internal/screens/evaluations/form_view.go index 3b3f2462..db75d2ec 100644 --- a/packages/tui/internal/screens/evaluations/form_view.go +++ b/packages/tui/internal/screens/evaluations/form_view.go @@ -7,6 +7,22 @@ import ( "github.com/rogue/tui/internal/theme" ) +// authTypeDisplayName returns the human-readable label for an auth type string +func authTypeDisplayName(authType string) string { + switch authType { + case "no_auth", "": + return "No Auth" + case "api_key": + return "API Key" + case "bearer_token": + return "Bearer Token" + case "basic": + return "Basic Auth" + default: + return authType + } +} + // RenderForm renders the new evaluation form screen func RenderForm(state *FormState) string { t := theme.CurrentTheme() @@ -186,6 +202,9 @@ func RenderForm(state *FormState) string { evalMode = "🔴 Red Team" } + // Prepare auth type display value + authTypeDisplay := authTypeDisplayName(state.AuthType) + // Prepare scan type display value (only for Red Team mode) scanTypeDisplay := "Basic" if state.ScanType == "full" { @@ -198,11 +217,11 @@ func RenderForm(state *FormState) string { } // Determine start button index based on mode - // Policy mode: StartButton at 6 - // Red Team mode: StartButton at 8 (after ScanType at 6, Configure at 7) - startButtonIndex := 6 + // Policy mode: StartButton at 8 + // Red Team mode: StartButton at 10 (after ScanType at 8, Configure at 9) + startButtonIndex := 8 if isRedTeam { - startButtonIndex = 8 + startButtonIndex = 10 } // Helper function to render the start button @@ -274,34 +293,41 @@ func RenderForm(state *FormState) string { // Build the content sections // Protocol is shown first, then Agent URL/Python File, then Transport (if applicable) isPythonProtocol := protocol == "python" + showAuthCredentials := !isPythonProtocol && state.AuthType != "no_auth" && state.AuthType != "" var formFields []string if isPythonProtocol { - // Python protocol: show Protocol, Python File, then other fields (no Transport) + // Python protocol: show Protocol, Python File, then other fields (no Transport/Auth) pythonFile := state.PythonEntrypointFile formFields = []string{ renderDropdownField(0, "Protocol:", protocol), renderTextField(1, "Python File:", pythonFile), - // No Transport field for Python protocol - renderTextField(3, "Judge LLM:", judge), - renderToggleField(4, "Deep Test:", deep), - renderDropdownField(5, "Mode:", evalMode), + // No Transport, AuthType, AuthCredentials for Python protocol + renderTextField(5, "Judge LLM:", judge), + renderToggleField(6, "Deep Test:", deep), + renderDropdownField(7, "Mode:", evalMode), } } else { - // A2A/MCP protocols: show Protocol, Agent URL, Transport, then other fields + // A2A/MCP/OpenAI protocols: show Protocol, Agent URL, Transport, Auth, then other fields formFields = []string{ renderDropdownField(0, "Protocol:", protocol), renderTextField(1, "Agent URL:", agent), renderDropdownField(2, "Transport:", transport), - renderTextField(3, "Judge LLM:", judge), - renderToggleField(4, "Deep Test:", deep), - renderDropdownField(5, "Mode:", evalMode), + renderDropdownField(3, "Auth Type:", authTypeDisplay), + } + if showAuthCredentials { + formFields = append(formFields, renderTextField(4, "Credentials:", state.AuthCredentials)) } + formFields = append(formFields, + renderTextField(5, "Judge LLM:", judge), + renderToggleField(6, "Deep Test:", deep), + renderDropdownField(7, "Mode:", evalMode), + ) } // Add ScanType dropdown and Configure button only in Red Team mode if isRedTeam { - formFields = append(formFields, renderDropdownField(6, "Scan Type:", scanTypeDisplay)) + formFields = append(formFields, renderDropdownField(int(FormFieldScanType), "Scan Type:", scanTypeDisplay)) // Configure button for custom scan configuration - styled like other fields configActive := state.CurrentField == int(FormFieldConfigureButton) diff --git a/packages/tui/internal/screens/evaluations/types.go b/packages/tui/internal/screens/evaluations/types.go index 42cc6fe2..fb5f6cbc 100644 --- a/packages/tui/internal/screens/evaluations/types.go +++ b/packages/tui/internal/screens/evaluations/types.go @@ -9,16 +9,18 @@ const ( FormFieldProtocol FormField = iota FormFieldAgentURL // Also used for PythonEntrypointFile when protocol is Python FormFieldTransport + FormFieldAuthType // Skipped for Python protocol + FormFieldAuthCredentials // Skipped for Python protocol; skipped when AuthType is no_auth FormFieldJudgeModel FormFieldDeepTest FormFieldEvaluationMode - // Policy mode start button / Red Team mode scan type (both at index 6) + // Policy mode start button / Red Team mode scan type (both at index 8) FormFieldStartButtonPolicy - FormFieldConfigureButton // 7 - Red Team mode only - FormFieldStartButtonRedTeam // 8 - Red Team mode only + FormFieldConfigureButton // 9 - Red Team mode only + FormFieldStartButtonRedTeam // 10 - Red Team mode only ) -// FormFieldScanType is an alias - in Red Team mode, index 6 is the scan type field +// FormFieldScanType is an alias - in Red Team mode, index 8 is the scan type field const FormFieldScanType = FormFieldStartButtonPolicy // FormState contains all data needed to render the evaluation form @@ -31,6 +33,8 @@ type FormState struct { AgentURL string Protocol string Transport string + AuthType string // "no_auth", "api_key", "bearer_token", "basic" + AuthCredentials string // Credential value (API key, token, or base64 username:password) PythonEntrypointFile string // Path to Python file with call_agent function (for Python protocol) JudgeModel string DeepTest bool @@ -40,9 +44,10 @@ type FormState struct { ScanType string // "basic", "full", or "custom" (only shown in Red Team mode) // Editing state - // Policy mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: JudgeModel, 4: DeepTest, 5: EvaluationMode, 6: StartButton - // Red Team mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: JudgeModel, 4: DeepTest, 5: EvaluationMode, 6: ScanType, 7: Configure, 8: StartButton - // Note: Transport field is skipped for Python protocol + // Policy mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: AuthType, 4: AuthCredentials, 5: JudgeModel, 6: DeepTest, 7: EvaluationMode, 8: StartButton + // Red Team mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: AuthType, 4: AuthCredentials, 5: JudgeModel, 6: DeepTest, 7: EvaluationMode, 8: ScanType, 9: Configure, 10: StartButton + // Note: Transport, AuthType, AuthCredentials fields are skipped for Python protocol + // Note: AuthCredentials field is skipped when AuthType is no_auth CurrentField int CursorPos int diff --git a/packages/tui/internal/tui/common_controller.go b/packages/tui/internal/tui/common_controller.go index 172033a6..96280962 100644 --- a/packages/tui/internal/tui/common_controller.go +++ b/packages/tui/internal/tui/common_controller.go @@ -48,7 +48,7 @@ func (m Model) handlePasteMsg(msg tea.PasteMsg) (Model, tea.Cmd) { return m, nil } - // Only paste into text fields (Agent URL/Python File and Judge Model) + // Only paste into text fields (Agent URL/Python File, Auth Credentials, and Judge Model) switch m.evalState.currentField { case EvalFieldAgentURL: // Insert at cursor position (for Agent URL or Python File depending on protocol) @@ -68,6 +68,22 @@ func (m Model) handlePasteMsg(msg tea.PasteMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + case EvalFieldAuthCredentials: + runes := []rune(m.evalState.AgentAuthCredentials) + m.evalState.AgentAuthCredentials = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:]) + m.evalState.cursorPos += len([]rune(cleanText)) + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldJudgeModel: // Insert at cursor position diff --git a/packages/tui/internal/tui/eval_form_view.go b/packages/tui/internal/tui/eval_form_view.go index 92ae8f9f..5cdc273f 100644 --- a/packages/tui/internal/tui/eval_form_view.go +++ b/packages/tui/internal/tui/eval_form_view.go @@ -32,6 +32,8 @@ func (m Model) RenderNewEvaluation() string { AgentURL: m.evalState.AgentURL, Protocol: string(m.evalState.AgentProtocol), Transport: string(m.evalState.AgentTransport), + AuthType: string(m.evalState.AgentAuthType), + AuthCredentials: m.evalState.AgentAuthCredentials, PythonEntrypointFile: m.evalState.PythonEntrypointFile, JudgeModel: m.evalState.JudgeModel, DeepTest: m.evalState.DeepTest, diff --git a/packages/tui/internal/tui/eval_types.go b/packages/tui/internal/tui/eval_types.go index 316f2315..a63c1457 100644 --- a/packages/tui/internal/tui/eval_types.go +++ b/packages/tui/internal/tui/eval_types.go @@ -45,23 +45,26 @@ const ( type EvalFormField int // EvalField constants for form field indices -// Policy mode: Protocol(0), AgentURL/PythonFile(1), Transport(2), JudgeModel(3), DeepTest(4), EvaluationMode(5), StartButton(6) -// Red Team mode adds: ScanType(6), ConfigureButton(7), StartButton(8) -// Note: Transport field is skipped for Python protocol +// Policy mode: Protocol(0), AgentURL/PythonFile(1), Transport(2), AuthType(3), AuthCredentials(4), JudgeModel(5), DeepTest(6), EvaluationMode(7), StartButton(8) +// Red Team mode adds: ScanType(8), ConfigureButton(9), StartButton(10) +// Note: Transport, AuthType, AuthCredentials fields are skipped for Python protocol +// Note: AuthCredentials field is skipped when AuthType is no_auth const ( EvalFieldProtocol EvalFormField = iota EvalFieldAgentURL // Also used for PythonEntrypointFile when protocol is Python EvalFieldTransport + EvalFieldAuthType // Skipped for Python protocol + EvalFieldAuthCredentials // Skipped for Python protocol; skipped when AuthType is no_auth EvalFieldJudgeModel EvalFieldDeepTest EvalFieldEvaluationMode - // Policy mode start button / Red Team mode scan type (both at index 6) + // Policy mode start button / Red Team mode scan type (both at index 8) EvalFieldStartButtonPolicy - EvalFieldConfigureButton // 7 - Red Team mode only - EvalFieldStartButtonRedTeam // 8 - Red Team mode only + EvalFieldConfigureButton // 9 - Red Team mode only + EvalFieldStartButtonRedTeam // 10 - Red Team mode only ) -// EvalFieldScanType is an alias - in Red Team mode, index 6 is the scan type field +// EvalFieldScanType is an alias - in Red Team mode, index 8 is the scan type field const EvalFieldScanType = EvalFieldStartButtonPolicy // ScanType represents the type of red team scan @@ -89,6 +92,8 @@ type EvaluationViewState struct { AgentURL string AgentProtocol Protocol AgentTransport Transport + AgentAuthType AuthType + AgentAuthCredentials string PythonEntrypointFile string // Path to Python file with call_agent function (for Python protocol) JudgeModel string ParallelRuns int @@ -121,9 +126,10 @@ type EvaluationViewState struct { StructuredSummary StructuredSummary // Editing state for New Evaluation - // Policy mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: JudgeModel, 4: DeepTest, 5: EvaluationMode, 6: StartButton - // Red Team mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: JudgeModel, 4: DeepTest, 5: EvaluationMode, 6: ScanType, 7: ConfigureButton, 8: StartButton - // Note: Transport field is skipped for Python protocol + // Policy mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: AuthType, 4: AuthCredentials, 5: JudgeModel, 6: DeepTest, 7: EvaluationMode, 8: StartButton + // Red Team mode: 0: Protocol, 1: AgentURL/PythonFile, 2: Transport, 3: AuthType, 4: AuthCredentials, 5: JudgeModel, 6: DeepTest, 7: EvaluationMode, 8: ScanType, 9: ConfigureButton, 10: StartButton + // Note: Transport, AuthType, AuthCredentials fields are skipped for Python protocol + // Note: AuthCredentials field is skipped when AuthType is no_auth currentField EvalFormField // Field index for form navigation cursorPos int // rune index in current text field } @@ -142,6 +148,8 @@ type UserConfigFromFile struct { PythonEntrypointFile string `json:"python_entrypoint_file"` EvaluationMode string `json:"evaluation_mode"` ScanType string `json:"scan_type"` + AuthType string `json:"auth_type"` + AuthCredentials string `json:"auth_credentials"` } // loadUserConfigFromWorkdir reads .rogue/user_config.json upward from CWD @@ -184,8 +192,8 @@ func findUserConfigPath() string { return filepath.Join(wd, ".rogue", "user_config.json") } -// saveUserConfig saves the protocol, evaluation mode, scan type and other settings to user_config.json -func saveUserConfig(protocol Protocol, transport Transport, agentURL, pythonEntrypointFile string, evaluationMode EvaluationMode, scanType ScanType) error { +// saveUserConfig saves the protocol, evaluation mode, scan type, auth settings and other settings to user_config.json +func saveUserConfig(protocol Protocol, transport Transport, agentURL, pythonEntrypointFile string, evaluationMode EvaluationMode, scanType ScanType, authType AuthType, authCredentials string) error { configPath := findUserConfigPath() // Read existing config to preserve other fields @@ -198,11 +206,15 @@ func saveUserConfig(protocol Protocol, transport Transport, agentURL, pythonEntr existingData["protocol"] = string(protocol) if protocol == ProtocolPython { existingData["python_entrypoint_file"] = pythonEntrypointFile - // Clear transport for Python protocol + // Clear transport and auth for Python protocol delete(existingData, "transport") + delete(existingData, "auth_type") + delete(existingData, "auth_credentials") } else { existingData["transport"] = string(transport) existingData["evaluated_agent_url"] = agentURL + existingData["auth_type"] = string(authType) + existingData["auth_credentials"] = authCredentials // Clear python entrypoint for non-Python protocols delete(existingData, "python_entrypoint_file") } @@ -304,6 +316,11 @@ func LoadEvaluationStateFromConfig(appConfig *config.Config) (*EvaluationViewSta if userConfig.Transport != "" { agentTransport = Transport(userConfig.Transport) } + agentAuthType := AuthTypeNoAuth + if userConfig.AuthType != "" { + agentAuthType = AuthType(userConfig.AuthType) + } + agentAuthCredentials := userConfig.AuthCredentials pythonEntrypointFile := userConfig.PythonEntrypointFile // Load evaluation mode from config @@ -384,6 +401,8 @@ func LoadEvaluationStateFromConfig(appConfig *config.Config) (*EvaluationViewSta AgentURL: agentURL, AgentProtocol: agentProtocol, AgentTransport: agentTransport, + AgentAuthType: agentAuthType, + AgentAuthCredentials: agentAuthCredentials, PythonEntrypointFile: pythonEntrypointFile, JudgeModel: judgeModel, ParallelRuns: 1, @@ -424,6 +443,8 @@ func (m *Model) startEval(ctx context.Context, st *EvaluationViewState) { st.AgentURL, st.AgentProtocol, st.AgentTransport, + st.AgentAuthType, + st.AgentAuthCredentials, st.Scenarios, st.JudgeModel, st.ParallelRuns, @@ -575,6 +596,37 @@ func (st *EvaluationViewState) cycleTransport(reverse bool) { st.AgentTransport = transports[currentIdx] } +// getAllAuthTypes returns all available auth type options +func getAllAuthTypes() []AuthType { + return []AuthType{AuthTypeNoAuth, AuthTypeAPIKey, AuthTypeBearer, AuthTypeBasic} +} + +// cycleAuthType cycles to the next auth type option +func (st *EvaluationViewState) cycleAuthType(reverse bool) { + authTypes := getAllAuthTypes() + currentIdx := 0 + for i, a := range authTypes { + if a == st.AgentAuthType { + currentIdx = i + break + } + } + + if reverse { + currentIdx-- + if currentIdx < 0 { + currentIdx = len(authTypes) - 1 + } + } else { + currentIdx++ + if currentIdx >= len(authTypes) { + currentIdx = 0 + } + } + + st.AgentAuthType = authTypes[currentIdx] +} + // cycleEvaluationMode cycles between Policy and Red Team modes func (st *EvaluationViewState) cycleEvaluationMode(reverse bool) { if reverse { diff --git a/packages/tui/internal/tui/form_controller.go b/packages/tui/internal/tui/form_controller.go index 42c64a1a..6bd3d32d 100644 --- a/packages/tui/internal/tui/form_controller.go +++ b/packages/tui/internal/tui/form_controller.go @@ -17,8 +17,17 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.RedTeamConfigSaved = false if m.evalState.currentField > 0 { m.evalState.currentField-- - // Skip Transport field for Python protocol - if m.evalState.AgentProtocol == ProtocolPython && m.evalState.currentField == EvalFieldTransport { + // Skip Transport, AuthType, AuthCredentials for Python protocol (going up) + for m.evalState.AgentProtocol == ProtocolPython && + (m.evalState.currentField == EvalFieldTransport || + m.evalState.currentField == EvalFieldAuthType || + m.evalState.currentField == EvalFieldAuthCredentials) { + m.evalState.currentField-- + } + // Skip AuthCredentials when AuthType is no_auth (going up) + if m.evalState.AgentProtocol != ProtocolPython && + m.evalState.AgentAuthType == AuthTypeNoAuth && + m.evalState.currentField == EvalFieldAuthCredentials { m.evalState.currentField-- } // Set cursor to end of field content when switching fields @@ -29,6 +38,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { } else { m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) } + case EvalFieldAuthCredentials: + m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) case EvalFieldJudgeModel: m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) default: @@ -44,8 +55,17 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { maxFieldIndex := m.evalState.getMaxFieldIndex() if m.evalState.currentField < maxFieldIndex { m.evalState.currentField++ - // Skip Transport field for Python protocol - if m.evalState.AgentProtocol == ProtocolPython && m.evalState.currentField == EvalFieldTransport { + // Skip Transport, AuthType, AuthCredentials for Python protocol (going down) + for m.evalState.AgentProtocol == ProtocolPython && + (m.evalState.currentField == EvalFieldTransport || + m.evalState.currentField == EvalFieldAuthType || + m.evalState.currentField == EvalFieldAuthCredentials) { + m.evalState.currentField++ + } + // Skip AuthCredentials when AuthType is no_auth (going down) + if m.evalState.AgentProtocol != ProtocolPython && + m.evalState.AgentAuthType == AuthTypeNoAuth && + m.evalState.currentField == EvalFieldAuthCredentials { m.evalState.currentField++ } // Set cursor to end of field content when switching fields @@ -56,6 +76,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { } else { m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) } + case EvalFieldAuthCredentials: + m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) case EvalFieldJudgeModel: m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) default: @@ -68,7 +90,11 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { // Clear the red team config saved banner when user starts interacting m.evalState.RedTeamConfigSaved = false switch m.evalState.currentField { - case EvalFieldAgentURL, EvalFieldJudgeModel: // Text fields + case EvalFieldAgentURL, EvalFieldJudgeModel: // Text fields - move cursor left + if m.evalState.cursorPos > 0 { + m.evalState.cursorPos-- + } + case EvalFieldAuthCredentials: // Text field - move cursor left if m.evalState.cursorPos > 0 { m.evalState.cursorPos-- } @@ -82,6 +108,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldTransport: m.evalState.cycleTransport(true) // cycle backwards @@ -93,6 +121,21 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + case EvalFieldAuthType: + m.evalState.cycleAuthType(true) // cycle backwards + // Save config after auth type change + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldEvaluationMode: m.evalState.cycleEvaluationMode(true) // cycle backwards @@ -104,6 +147,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldScanType: // ScanType dropdown (only in Red Team mode) if m.evalState.EvaluationMode == EvaluationModeRedTeam { @@ -118,6 +163,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) } } @@ -137,6 +184,11 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { if m.evalState.cursorPos < fieldLen { m.evalState.cursorPos++ } + case EvalFieldAuthCredentials: // Text field - move cursor right + fieldLen := len([]rune(m.evalState.AgentAuthCredentials)) + if m.evalState.cursorPos < fieldLen { + m.evalState.cursorPos++ + } case EvalFieldProtocol: m.evalState.cycleProtocol(false) // cycle forwards // Save config after protocol change @@ -147,6 +199,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldTransport: m.evalState.cycleTransport(false) // cycle forwards @@ -158,6 +212,21 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + case EvalFieldAuthType: + m.evalState.cycleAuthType(false) // cycle forwards + // Save config after auth type change + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldJudgeModel: fieldLen := len(m.evalState.JudgeModel) @@ -174,6 +243,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldScanType: // ScanType dropdown (only in Red Team mode) if m.evalState.EvaluationMode == EvaluationModeRedTeam { @@ -188,6 +259,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) } } @@ -229,6 +302,8 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) } } else { @@ -248,9 +323,30 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) } } + case EvalFieldAuthCredentials: + runes := []rune(m.evalState.AgentAuthCredentials) + if m.evalState.cursorPos > len(runes) { + m.evalState.cursorPos = len(runes) + } + if m.evalState.cursorPos > 0 && len(runes) > 0 { + m.evalState.AgentAuthCredentials = string(runes[:m.evalState.cursorPos-1]) + string(runes[m.evalState.cursorPos:]) + m.evalState.cursorPos-- + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + } case EvalFieldJudgeModel: runes := []rune(m.evalState.JudgeModel) // Clamp cursor position to valid range @@ -295,6 +391,25 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + case EvalFieldAuthCredentials: + runes := []rune(m.evalState.AgentAuthCredentials) + if m.evalState.cursorPos > len(runes) { + m.evalState.cursorPos = len(runes) + } + m.evalState.AgentAuthCredentials = string(runes[:m.evalState.cursorPos]) + s + string(runes[m.evalState.cursorPos:]) + m.evalState.cursorPos++ + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldJudgeModel: runes := []rune(m.evalState.JudgeModel) diff --git a/packages/tui/internal/tui/keyboard_controller.go b/packages/tui/internal/tui/keyboard_controller.go index fe1a9ffe..5dad6a83 100644 --- a/packages/tui/internal/tui/keyboard_controller.go +++ b/packages/tui/internal/tui/keyboard_controller.go @@ -167,7 +167,9 @@ func (m Model) handleGlobalSlash(msg tea.KeyMsg) (Model, tea.Cmd) { // Check if we're editing text fields that might need "/" character // Don't intercept "/" if we're editing text in NewEvaluationScreen if m.currentScreen == NewEvaluationScreen && m.evalState != nil && - (m.evalState.currentField == EvalFieldAgentURL || m.evalState.currentField == EvalFieldJudgeModel) { + (m.evalState.currentField == EvalFieldAgentURL || + m.evalState.currentField == EvalFieldAuthCredentials || + m.evalState.currentField == EvalFieldJudgeModel) { // Handle "/" character directly in text fields s := "/" switch m.evalState.currentField { @@ -197,6 +199,25 @@ func (m Model) handleGlobalSlash(msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.PythonEntrypointFile, m.evalState.EvaluationMode, m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, + ) + case EvalFieldAuthCredentials: + runes := []rune(m.evalState.AgentAuthCredentials) + if m.evalState.cursorPos > len(runes) { + m.evalState.cursorPos = len(runes) + } + m.evalState.AgentAuthCredentials = string(runes[:m.evalState.cursorPos]) + s + string(runes[m.evalState.cursorPos:]) + m.evalState.cursorPos++ + go saveUserConfig( + m.evalState.AgentProtocol, + m.evalState.AgentTransport, + m.evalState.AgentURL, + m.evalState.PythonEntrypointFile, + m.evalState.EvaluationMode, + m.getScanType(), + m.evalState.AgentAuthType, + m.evalState.AgentAuthCredentials, ) case EvalFieldJudgeModel: runes := []rune(m.evalState.JudgeModel) diff --git a/packages/tui/internal/tui/utils.go b/packages/tui/internal/tui/utils.go index 004ccfe1..2c0eba05 100644 --- a/packages/tui/internal/tui/utils.go +++ b/packages/tui/internal/tui/utils.go @@ -20,7 +20,8 @@ type AuthType string const ( AuthTypeNoAuth AuthType = "no_auth" - AuthTypeBearer AuthType = "bearer" + AuthTypeAPIKey AuthType = "api_key" + AuthTypeBearer AuthType = "bearer_token" AuthTypeBasic AuthType = "basic" ) @@ -36,7 +37,7 @@ type AgentConfig struct { EvaluatedAgentProtocol Protocol `json:"protocol"` EvaluatedAgentTransport Transport `json:"transport,omitempty"` EvaluatedAgentAuthType AuthType `json:"evaluated_agent_auth_type"` - EvaluatedAgentCredentials string `json:"evaluated_agent_credentials,omitempty"` + EvaluatedAgentCredentials string `json:"evaluated_agent_auth_credentials,omitempty"` PythonEntrypointFile string `json:"python_entrypoint_file,omitempty"` JudgeLLMModel string `json:"judge_llm"` JudgeLLMAPIKey string `json:"judge_llm_api_key,omitempty"` @@ -591,6 +592,8 @@ func (m *Model) StartEvaluation( agentURL string, agentProtocol Protocol, agentTransport Transport, + agentAuthType AuthType, + agentAuthCredentials string, scenarios []EvalScenario, judgeModel string, parallelRuns int, @@ -666,7 +669,8 @@ func (m *Model) StartEvaluation( // Build evaluation request agentConfig := AgentConfig{ EvaluatedAgentProtocol: agentProtocol, - EvaluatedAgentAuthType: AuthTypeNoAuth, + EvaluatedAgentAuthType: agentAuthType, + EvaluatedAgentCredentials: agentAuthCredentials, JudgeLLMModel: judgeModel, JudgeLLMAPIKey: apiKey, JudgeLLMAWSAccessKeyID: awsAccessKeyID, From 4cad8033ffe77c35c84e2b4a065bc46d0aeb8cd5 Mon Sep 17 00:00:00 2001 From: Yuval Date: Fri, 20 Feb 2026 12:57:23 +0200 Subject: [PATCH 4/4] Support circular scroll in eval --- packages/tui/internal/tui/form_controller.go | 62 +++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/tui/internal/tui/form_controller.go b/packages/tui/internal/tui/form_controller.go index 6bd3d32d..c99b6630 100644 --- a/packages/tui/internal/tui/form_controller.go +++ b/packages/tui/internal/tui/form_controller.go @@ -30,21 +30,24 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.currentField == EvalFieldAuthCredentials { m.evalState.currentField-- } - // Set cursor to end of field content when switching fields - switch m.evalState.currentField { - case EvalFieldAgentURL: - if m.evalState.AgentProtocol == ProtocolPython { - m.evalState.cursorPos = len([]rune(m.evalState.PythonEntrypointFile)) - } else { - m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) - } - case EvalFieldAuthCredentials: - m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) - case EvalFieldJudgeModel: - m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) - default: - m.evalState.cursorPos = 0 + } else { + // Wrap around: jump from top field to the start button + m.evalState.currentField = m.evalState.getStartButtonIndex() + } + // Set cursor to end of field content when switching fields + switch m.evalState.currentField { + case EvalFieldAgentURL: + if m.evalState.AgentProtocol == ProtocolPython { + m.evalState.cursorPos = len([]rune(m.evalState.PythonEntrypointFile)) + } else { + m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) } + case EvalFieldAuthCredentials: + m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) + case EvalFieldJudgeModel: + m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) + default: + m.evalState.cursorPos = 0 } return m, nil @@ -68,21 +71,24 @@ func HandleEvalFormInput(m Model, msg tea.KeyMsg) (Model, tea.Cmd) { m.evalState.currentField == EvalFieldAuthCredentials { m.evalState.currentField++ } - // Set cursor to end of field content when switching fields - switch m.evalState.currentField { - case EvalFieldAgentURL: - if m.evalState.AgentProtocol == ProtocolPython { - m.evalState.cursorPos = len([]rune(m.evalState.PythonEntrypointFile)) - } else { - m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) - } - case EvalFieldAuthCredentials: - m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) - case EvalFieldJudgeModel: - m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) - default: - m.evalState.cursorPos = 0 + } else { + // Wrap around: jump from start button back to the top field + m.evalState.currentField = EvalFieldProtocol + } + // Set cursor to end of field content when switching fields + switch m.evalState.currentField { + case EvalFieldAgentURL: + if m.evalState.AgentProtocol == ProtocolPython { + m.evalState.cursorPos = len([]rune(m.evalState.PythonEntrypointFile)) + } else { + m.evalState.cursorPos = len([]rune(m.evalState.AgentURL)) } + case EvalFieldAuthCredentials: + m.evalState.cursorPos = len([]rune(m.evalState.AgentAuthCredentials)) + case EvalFieldJudgeModel: + m.evalState.cursorPos = len([]rune(m.evalState.JudgeModel)) + default: + m.evalState.cursorPos = 0 } return m, nil