Skip to content
2 changes: 2 additions & 0 deletions doc/code/framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ See [message normalizers](./targets/11_message_normalizer) for capability behavi

- If you are creating a component with user input (e.g. via config, REST, or automatically) it should always use the registry
- If you are storing an instance of a component, it should always use the registry
- Construction does not require registration. The caller owns temporary instances
and their cleanup; the registry does not retain them.
- The registry accepts only explicitly supported external inputs, permits opaque Python objects only for in-process callers, and leaves component validation to constructors.

## [Setup](./setup/0_setup)
Expand Down
56 changes: 56 additions & 0 deletions doc/code/registry/0_registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,62 @@ creation can build more than one component for the same name, but only one
registration succeeds unless replacement is requested. The backend does not
need a separate registration lock.

### Construction without registration

Use `create_instance(type_name, **params)` to build an object without adding it
to `.instances`. The caller owns that object and its cleanup.
`create_named_instance(...)` remains the build-and-register operation.

The existing `POST /api/targets`, `POST /api/converters`, and `POST /api/scorers`
endpoints accept `register: false`. Registration defaults to `true`, so existing
clients keep their behavior. For example:

```json
{
"type": "CaesarConverter",
"register": false,
"params": { "caesar_offset": 3 }
}
```

The response contains an `identifier` and, for targets, `capabilities`. It does
not contain a registry name or a reusable object handle. The backend discards
the built object after returning its descriptor.

Targets and converters can also use a registered source that supports
reconstruction:

```json
{
"type": "OpenAIChatTarget",
"register": false,
"source": {
"source_name": "objective",
"source_hash": "<source identifier hash>",
"params": { "temperature": 0.7 }
}
}
```

Use `source.params` for overrides; do not also supply `params`. The backend
keeps the source's other constructor inputs, including authentication, on the
server. It does not change the source. Missing, changed, or ambiguous sources
produce an error. If a source was renamed, a unique matching hash can resolve it.
An optional `effective_hash` checks the reconstructed object's identity.
Registered descriptors report `reconstructable` and, for targets,
`supports_temperature_override`. Unsupported inputs can also report
`reconstruction_error`.
Constructor inputs that are not in the registry's build contract are not
discarded silently: reconstruction is rejected. Such sources can still be used
as registered objects.
Reconstruction retains resolved defaults from the declared constructor chain,
including explicitly forwarded parent parameters. Arguments generated inside a
constructor are not treated as extra inputs from its caller.

This REST flag applies to targets, converters, and scorers. `AttackRegistry`
remains a class catalog, not a store of attack instances, and has no new REST
endpoint.

Constructor annotations define parameter metadata and coercion. Enum parameters
accept member names or values. Types that inherit `StructuredParameterValue` declare their
allowed variants through `get_registry_input_variants()`; the registry
Expand Down
43 changes: 41 additions & 2 deletions doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ The Chat view is the primary workspace for running interactive attacks against c

#### Sending Messages

For a new chat, your default objective target is preselected if it is available. Click the target badge in the shared toolbar beside the label controls to open the target dropdown. If no target is selected, click **Select a target** in the same place. Your choice applies to this chat without changing the default. Saved chats keep their original target. An attack saved without a target uses this dropdown until its first send binds the selected target.
For a new chat, your default objective target is preselected if it is available. Click the target badge in the chat ribbon below the common labels and above the objective to open the target dropdown. If no target is selected, click **Select a target** in the same place. Your choice applies to this chat without changing the default. Saved chats keep their original target. An attack saved without a target uses this dropdown until its first send binds the selected target.

Clicking **Chat** while already in a new chat keeps its target and draft. Starting
a new attack resets both. Default changes in another tab apply to the next new
Expand All @@ -112,6 +112,22 @@ Type a message and press Enter (or click Send) to send it to the chat target. Th

When you open a saved chat, CoPyRIT automatically selects the target originally used, if its registered identity still matches. This also applies to direct links, reloads, and browser Back/Forward navigation. You can continue the same conversation without selecting the target again. Opening a saved chat does not change your defaults.

#### Temperature

Select a target, then set **Temperature** before the first send. Leave it empty
to keep the source setting. The supported range is 0 to 2. This creates a private
target configuration for the attack; it does not add or change a registered target.
Temperature is read-only after the attack is bound. In the conversation editor,
a temperature change requires **New attack**, not **Same attack**.
Hover over or click the read-only field to see why it cannot be changed.

Saved attacks retain the source name, source identity, temperature, and effective
identity, but not credentials. After a restart, the backend tries to reconstruct
the target from a matching registered source. If reconstruction fails, sending
is blocked rather than using the source's default temperature. OpenAI-family
targets with a temperature parameter support this control. Externally owned HTTP
clients and temperature set through `extra_body_parameters` are not supported.

#### Repeating a Message

Use **n=1** beside Send to choose **1 to 10** repetitions. Enter and Send use the
Expand Down Expand Up @@ -161,6 +177,20 @@ messages endpoint and `send=false` context storage are unchanged.

Open **Converters** and use the picker above the working input to add registered
converters in the order you want them to run.
For a stage that supports reconstruction, open **... > Settings** to change its
constructor settings. Changes create a private converter for that stage. Other
stages and the registered source keep their settings. **Reset to registered
converter** removes the stage's overrides. **Use default / not set** clears a
structured setting's override and restores the registered source's value.
Changing settings invalidates that stage and its downstream results.

Closing the converter pane discards temporary settings, but keeps content that
you already applied and the identifiers of the converters that produced it.
Temporary converters are built for each preview operation, not retained in a
second registry. A runtime change clears temporary settings and preview results.
Apply temporary converter results with **Add converted value** before repeating
a message. Independent repeated conversion supports registered stages only.

The top text box is an editable working copy: changing it does not change the original
chat message. The top **Convert** button runs the active tab's configured pipeline
and any configured inputs that do not have results yet. After every configured input
Expand Down Expand Up @@ -249,6 +279,15 @@ represents a manual conversion. `request_converter_configurations` controls
conversion of pieces without a preconverted value; it does not describe which
converters already ran.

For temporary stages, preview requests provide `converter_specs`, aligned with
`converter_ids`; use `null` for a registered stage. Preview responses include the
actual temporary identifier and a signed `provenance` token. Pass these tokens
as `applied_converter_provenance`, aligned with `applied_converter_ids`, when
sending or saving the converted content. Tokens remain valid after the pane
closes, without retaining converter objects. A restart invalidates tokens for
unsaved content; convert it again. Stored messages retain their converter
identifiers and do not depend on those tokens.

#### Attachments

Click the attachment button to add images, audio, video, or documents to your message. Supported types include `image/*`, `audio/*`, `video/*`, `.pdf`, `.doc`, `.docx`, and `.txt`. Attachments are displayed as chips below the input with type icons and file sizes.
Expand Down Expand Up @@ -371,7 +410,7 @@ Export stays available for read-only historical conversations, and is disabled w

The labels bar above the page content is available across the GUI, including scanner setup, Home, Chat, and History. It shows the active labels for future attacks and scans, not the attribution of a historical run you are viewing. Click the labels icon to open **Default Labels** and add, edit, or remove custom labels. The required `operator` and `operation` controls remain in the bar, outside this popover, and cannot be removed. A signed-in operator is read-only.

In Chat, the active target, Markdown toggle, export menu, conversations panel toggle, and **New Attack** button share the right side of this bar. They wrap below the labels on narrow screens.
In Chat, a separate ribbon below the labels contains the target, temperature, and **Edit Conversation** controls on the left. The Markdown toggle, export menu, conversations panel toggle, and **New Attack** button are on the right.

Clicking the `operation` label opens a picker listing the operations already recorded in memory, so you can choose one without typing it from memory. Typing a name that doesn't exist yet offers to create it. Very long lists show the first 200 and say how many are left, so type to narrow them. On narrow screens, use the labels icon to view or edit labels that do not fit inline.

Expand Down
32 changes: 23 additions & 9 deletions frontend/src/App.labels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,20 +247,34 @@ describe('Shared new run labels', () => {
}))
})

it('hosts Chat controls beside the labels and removes them when navigating away', async () => {
it('hosts Chat controls below the labels and above the objective, and removes them when navigating away', async () => {
const user = userEvent.setup()
jest.mocked(targetsApi.listTargets).mockResolvedValue({
items: [{ ...TARGET, supports_temperature_override: true }],
pagination: { limit: 200, has_more: false },
})
renderApp('/chat')
const toolbar = within(currentLabels()).getByRole('group', { name: 'Chat controls' })
const targetPicker = await within(toolbar).findByRole('combobox', { name: 'Chat target' })
const toolbar = screen.getByRole('group', { name: 'Chat controls' })
expect(currentLabels()).not.toContainElement(toolbar)
expect(currentLabels().compareDocumentPosition(toolbar) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(toolbar.compareDocumentPosition(screen.getByTestId('objective-header')) & Node.DOCUMENT_POSITION_FOLLOWING)
.toBeTruthy()
const settings = within(toolbar).getByRole('group', { name: 'Conversation settings' })
const actions = within(toolbar).getByRole('group', { name: 'Conversation actions' })
const targetPicker = await within(settings).findByRole('combobox', { name: 'Chat target' })
await waitFor(() => expect(targetPicker).toBeEnabled())
await user.selectOptions(targetPicker, 'test_target')
expect(targetPicker).toHaveValue('test_target')
const temperature = within(settings).getByRole('spinbutton', { name: 'Temperature' })
await user.type(temperature, '0.7')
expect(temperature).toHaveValue(0.7)
expect(within(settings).getByRole('button', { name: 'Edit Conversation' })).toBeEnabled()
expect(within(screen.getByTestId('chat-area')).queryByRole('group', { name: 'Chat controls' }))
.not.toBeInTheDocument()
expect(within(toolbar).getByRole('button', { name: 'Export conversation' })).toBeDisabled()
expect(within(toolbar).getByRole('button', { name: 'Toggle conversations panel' })).toBeDisabled()
expect(within(toolbar).getByRole('button', { name: 'New Attack' })).toBeDisabled()
const markdown = within(toolbar).getByRole('switch')
expect(within(actions).getByRole('button', { name: 'Export conversation' })).toBeDisabled()
expect(within(actions).getByRole('button', { name: 'Toggle conversations panel' })).toBeDisabled()
expect(within(actions).getByRole('button', { name: 'New Attack' })).toBeDisabled()
const markdown = within(actions).getByRole('switch')
await user.click(markdown)
expect(readUserPreferences('local').chatMarkdown).toBe(true)

Expand All @@ -271,7 +285,7 @@ describe('Shared new run labels', () => {
expect(screen.queryByRole('group', { name: 'Chat controls' })).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Chat', exact: true }))
expect(screen.getAllByRole('group', { name: 'Chat controls' })).toHaveLength(1)
expect(within(currentLabels()).getByRole('switch')).toBeChecked()
expect(within(screen.getByRole('group', { name: 'Conversation actions' })).getByRole('switch')).toBeChecked()
})

it('preserves an edit made before the backend defaults arrive', async () => {
Expand Down Expand Up @@ -330,7 +344,7 @@ describe('Shared new run labels', () => {
expect(attacksApi.createAttack).not.toHaveBeenCalled()
expect(attacksApi.submitMessageSend).not.toHaveBeenCalled()

const toolbar = within(currentLabels()).getByRole('group', { name: 'Chat controls' })
const toolbar = screen.getByRole('group', { name: 'Chat controls' })
expect(within(toolbar).getByLabelText('Active target: test_target')).toBeInTheDocument()
await waitFor(() => expect(within(toolbar).getByRole('button', { name: 'Export conversation' })).toBeEnabled())
await user.click(within(toolbar).getByRole('button', { name: 'Export conversation' }))
Expand Down
1 change: 1 addition & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ function AppContent({ operatorAlias }: { operatorAlias: string | null }) {
endpoint: targetEndpoint(createdTarget),
model_name: targetModelName(createdTarget),
identifier_hash: targetIdentifierHash(createdTarget),
binding: createdTarget.binding,
}
: null
skipNextLoadForAttackId.current = arId
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/components/Chat/ChatWindow.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,34 @@ export const useChatWindowStyles = makeStyles({
flexGrow: 1,
flexWrap: 'wrap',
gap: tokens.spacingHorizontalM,
minWidth: 0,
maxWidth: '100%',
},
conversationControls: {
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: tokens.spacingHorizontalM,
marginRight: 'auto',
minWidth: 0,
maxWidth: '100%',
},
editActions: {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: 'auto',
gap: tokens.spacingHorizontalXS,
},
temperatureField: {
gridTemplateColumns: 'max-content 5rem',
columnGap: tokens.spacingHorizontalNone,
alignItems: 'center',
flexShrink: 0,
},
temperatureInput: {
width: '5rem',
minWidth: 0,
},
sharedTarget: {
maxWidth: '240px',
[NARROW_VIEWPORT_QUERY]: {
Expand All @@ -123,6 +142,7 @@ export const useChatWindowStyles = makeStyles({
flexWrap: 'wrap',
justifyContent: 'flex-end',
minWidth: 0,
marginLeft: 'auto',
},
ribbonAction: {
...mobileTouchTarget,
Expand Down
Loading
Loading