Skip to content
Open
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
23 changes: 22 additions & 1 deletion frontend/src/components/Config/CreateTargetDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ const TARGET_TYPES: TargetTypeListResponse = {
supported_auth_modes: ["api_key"],
description: "A prompt target that distributes requests across multiple inner targets using weighted round-robin selection.",
},
{
target_type: "TextTarget",
parameters: [],
supported_auth_modes: [],
description: "Writes prompts to a local text stream.",
},
{
target_type: "HTTPTarget",
parameters: [
Expand Down Expand Up @@ -346,6 +352,7 @@ const TARGET_DISPLAY_NAMES: Record<string, string> = {
OpenAIVideoTarget: "OpenAI video",
RoundRobinTarget: "Weighted round robin",
HTTPTarget: "HTTPTarget",
TextTarget: "TextTarget",
AzureBlobStorageTarget: "AzureBlobStorageTarget",
};

Expand Down Expand Up @@ -542,7 +549,7 @@ describe("CreateTargetDialog", () => {
await openTargetTypePicker();

const options = screen.getAllByRole("option");
expect(options).toHaveLength(11);
expect(options).toHaveLength(12);
for (const entry of TARGET_TYPES.items.filter((item) => item.target_type !== "PlaywrightTarget")) {
const option = screen.getByRole("option", {
name: new RegExp(`Implementation: ${entry.target_type}`),
Expand Down Expand Up @@ -583,6 +590,20 @@ describe("CreateTargetDialog", () => {
expect(selectedDetails).toHaveTextContent("Supported authentication: API key");
});

it("should say a credential-free target needs no authentication", async () => {
render(
<TestWrapper>
<CreateTargetDialog {...defaultProps} />
</TestWrapper>
);

await selectTargetType("TextTarget");

const selectedDetails = screen.getByRole("region", { name: /selected target details/i });
expect(selectedDetails).toHaveTextContent("No authentication required");
expect(selectedDetails).not.toHaveTextContent("API key");
});

it("should keep the selected target displayed after focus moves to another field", async () => {
const user = userEvent.setup();

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/Config/CreateTargetDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,15 @@ function getTargetDisplayName(targetType: string): string {
return TARGET_DISPLAY_NAMES[targetType] ?? targetType
}

function getAuthDescription(entry: TargetTypeEntry): string | null {
function getAuthDescription(entry: TargetTypeEntry, registryMetadataLoaded: boolean): string | null {
if (entry.target_type === 'GitHubCopilotTarget') {
return 'Supported authentication: GitHub token'
}
const authModes = entry.supported_auth_modes
if (authModes.length === 0) return null
if (authModes.length === 0) {
// The built-in fallback entries also carry an empty list, so only registry data can say "no credentials".
return registryMetadataLoaded ? 'No authentication required' : null
}

const labels = authModes.map((mode) => (
mode === 'identity' ? 'Microsoft Entra ID' : 'API key'
Expand Down Expand Up @@ -379,7 +382,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated, existingT
const hasApiKeyField = hasField('api_key')
const selectedTargetDisplayName = getTargetDisplayName(targetType)
const selectedTargetAuthDescription = targetTypeEntry
? getAuthDescription(targetTypeEntry)
? getAuthDescription(targetTypeEntry, typeMetadataAvailable)
: null
const supportsIdentity = targetTypeEntry
? targetTypeEntry.supported_auth_modes.includes('identity')
Expand Down Expand Up @@ -669,7 +672,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated, existingT
>
{targetTypeOptions.map((entry) => {
const displayName = getTargetDisplayName(entry.target_type)
const authDescription = getAuthDescription(entry)
const authDescription = getAuthDescription(entry, typeMetadataAvailable)
const accessibleDescription = [
displayName,
entry.description,
Expand Down
6 changes: 5 additions & 1 deletion pyrit/prompt_target/text_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# Licensed under the MIT license.

import sys
from typing import IO
from typing import IO, ClassVar

from pyrit.common.auth_mode import AuthMode
from pyrit.models import Message
from pyrit.prompt_target.common.prompt_target import PromptTarget
from pyrit.prompt_target.common.target_configuration import TargetConfiguration
Expand All @@ -18,6 +19,9 @@ class TextTarget(PromptTarget):
but enter them manually.
"""

# Writes to a local stream and never contacts a service, so it takes no credentials.
supported_auth_modes: ClassVar[tuple[AuthMode, ...]] = ()

def __init__(
self,
*,
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/backend/test_target_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ async def test_types_include_declarative_auth_facts(self) -> None:
assert "api_key" in openai_entry.supported_auth_modes
assert "identity" in openai_entry.supported_auth_modes

async def test_types_report_no_auth_modes_for_credential_free_target(self) -> None:
"""TextTarget needs no credentials, so its type entry advertises no auth modes."""
service = TargetService()

result = await service.list_target_types_async()

entry = next(item for item in result.items if item.target_type == "TextTarget")
assert entry.supported_auth_modes == []

async def test_types_include_github_copilot_form_parameters_async(self) -> None:
service = TargetService()

Expand Down
6 changes: 5 additions & 1 deletion tests/unit/registry/test_target_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,11 @@ def test_round_robin_targets_param_is_reference(self, registry: TargetRegistry):
def test_metadata_supported_auth_modes_projects_class_attribute(self, registry: TargetRegistry):
# Identity-capable targets expose both modes; api-key-only targets expose one.
assert self._metadata_for(registry, "OpenAIChatTarget").supported_auth_modes == ("api_key", "identity")
assert self._metadata_for(registry, "TextTarget").supported_auth_modes == ("api_key",)
assert self._metadata_for(registry, "GitHubCopilotTarget").supported_auth_modes == ("api_key",)

def test_metadata_supported_auth_modes_empty_for_credential_free_target(self, registry: TargetRegistry):
# TextTarget writes to a local stream, so it must not advertise api-key auth.
assert self._metadata_for(registry, "TextTarget").supported_auth_modes == ()

def test_metadata_supported_auth_modes_sourced_from_class_attributes(self, registry: TargetRegistry):
# Auth modes are read off the class via Param.ClassAttr, not a fabricated instance.
Expand Down
Loading