Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9823509
initial, minimal working version of WebSocketCopilotTarget
paulinek13 Dec 22, 2025
62fc335
add useful error message for "server rejected WebSocket connection: H…
paulinek13 Dec 23, 2025
107b715
improve error handling and logging
paulinek13 Dec 23, 2025
41013a2
enhance WebSocket URL validation
paulinek13 Dec 23, 2025
c8e7e83
small fix
paulinek13 Dec 23, 2025
af636b2
fix
paulinek13 Dec 23, 2025
c9ebcee
improve `_parse_message`
paulinek13 Dec 23, 2025
99040d0
useful links
paulinek13 Dec 23, 2025
1f21ebf
add timeouts for responses and connection
paulinek13 Dec 24, 2025
1806e79
start with tests
paulinek13 Dec 25, 2025
3962bbb
require `wss://` only
paulinek13 Dec 25, 2025
7588e8d
add configurable response timeout
paulinek13 Dec 25, 2025
b98f675
fix
paulinek13 Dec 25, 2025
0dab7bb
replace Enum with IntEnum and actually use it
paulinek13 Dec 25, 2025
c2df619
test_dict_to_websocket_static_method
paulinek13 Dec 25, 2025
18fd238
fix
paulinek13 Dec 25, 2025
73b07d0
Refactor WebSocket message parser to handle multiple frames per message
paulinek13 Dec 25, 2025
9a8a878
rename message types in the enum
paulinek13 Dec 25, 2025
4d3c15d
add raw WebSocket messages for testing
paulinek13 Dec 25, 2025
b095d74
remove emojis
paulinek13 Dec 25, 2025
38e6868
simpler way to get the final result
paulinek13 Dec 25, 2025
2430dbe
log full raw message when no parseable content found
paulinek13 Dec 25, 2025
5b2c54a
_value2member_map_
paulinek13 Dec 25, 2025
4a7a7b8
TestParseRawMessage
paulinek13 Dec 25, 2025
acb0a6d
test fix
paulinek13 Dec 25, 2025
276290f
TODO: use msal for auth
paulinek13 Dec 25, 2025
ded56c6
add device code flow authentication method
paulinek13 Dec 26, 2025
558f48f
Revert "TODO: use msal for auth" -- as we need browser automation any…
paulinek13 Dec 26, 2025
02e3a4e
Revert "add device code flow authentication method"
paulinek13 Dec 26, 2025
0a9ee34
add Playwright-based way of getting sydney access token
paulinek13 Dec 27, 2025
cbc06f0
use `msal-extensions` for encrypted token persistence
paulinek13 Dec 28, 2025
4299673
add CopilotAuthenticator to WebSocketCopilotTarget for automated auth…
paulinek13 Dec 28, 2025
c65205b
WORKING multi prompt example
paulinek13 Dec 29, 2025
fd745ba
unit tests update for `WebSocketCopilotTarget`
paulinek13 Dec 29, 2025
fd8bc17
fixes
paulinek13 Dec 29, 2025
7266d7e
AUTH FLOW improvements
paulinek13 Dec 29, 2025
5241160
`CopilotAuthenticator` tests
paulinek13 Dec 30, 2025
dfa0ace
various fixes
paulinek13 Dec 30, 2025
f865698
notebook
paulinek13 Jan 11, 2026
913f7b6
fixing precommit stuff
paulinek13 Jan 11, 2026
806f37f
refresh_token_async/get_token_async
paulinek13 Jan 11, 2026
53c7e10
ProactorEventLoop && notebook rerun
paulinek13 Jan 11, 2026
b11370a
Merge branch 'main' into feat/343/websocket_copilot_target
paulinek13 Jan 11, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ cython_debug/

# PyRIT secrets file
.env
.pyrit_cache/

# Cache for generating docs
doc/generate_docs/cache/*
Expand Down
1 change: 1 addition & 0 deletions doc/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ chapters:
- file: code/targets/playwright_target_copilot
- file: code/targets/prompt_shield_target
- file: code/targets/use_huggingface_chat_target
- file: code/targets/websocket_copilot_target
- file: code/targets/realtime_target
- file: code/converters/0_converters
sections:
Expand Down
2 changes: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ API Reference
Authenticator
AzureAuth
AzureStorageAuth
CopilotAuthenticator

:py:mod:`pyrit.auxiliary_attacks`
=================================
Expand Down Expand Up @@ -515,6 +516,7 @@ API Reference
PromptTarget
RealtimeTarget
TextTarget
WebSocketCopilotTarget

:py:mod:`pyrit.score`
=====================
Expand Down
217 changes: 217 additions & 0 deletions doc/code/targets/websocket_copilot_target.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# WebSocket Copilot Target\n",
"\n",
"The `WebSocketCopilotTarget` is an alternative to the `PlaywrightCopilotTarget` that is designed to be more reliable by minimizing dependence on browser automation. Instead of driving the Copilot UI, it communicates directly with Copilot over a WebSocket connection, using Playwright only for authentication.\n",
"\n",
"Before using this target, ensure you have:\n",
"\n",
"1. A licensed Microsoft 365 Copilot account (the free version is not supported)\n",
"2. Playwright installed: `pip install playwright && playwright install chromium`\n",
"3. Set the following environment variables:\n",
" - `COPILOT_USERNAME`: Your Microsoft account username/email\n",
" - `COPILOT_PASSWORD`: Your Microsoft account password\n",
"\n",
"Note:\n",
"The `WebSocketCopilotTarget` uses `CopilotAuthenticator` under the hood, which launches a headless browser once to obtain authentication tokens. These tokens are then cached for subsequent requests and refreshed as needed."
]
},
{
"cell_type": "markdown",
"id": "1",
"metadata": {},
"source": [
"## Basic Usage with `PromptSendingAttack`\n",
"\n",
"The simplest way to interact with the `WebSocketCopilotTarget` is through the `PromptSendingAttack` class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[34m🔹 Turn 1 - USER\u001b[0m\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[34m Tell me a joke about AI\u001b[0m\n",
"\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[33m Here’s a lighthearted one for you:\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m **Why did the AI go broke?**\u001b[0m\n",
"\u001b[33m Because it kept working for *exposure*!\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m 😄 Want me to share a few more AI-themed jokes, or maybe something geeky and tech-related?\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n"
]
}
],
"source": [
"from pyrit.executor.attack import ConsoleAttackResultPrinter, PromptSendingAttack\n",
"from pyrit.prompt_target import WebSocketCopilotTarget\n",
"from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n",
"\n",
"await initialize_pyrit_async(memory_db_type=IN_MEMORY)\n",
"\n",
"target = WebSocketCopilotTarget()\n",
"attack = PromptSendingAttack(objective_target=target)\n",
"\n",
"objective = \"Tell me a joke about AI\"\n",
"\n",
"result = await attack.execute_async(objective=objective)\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result)"
]
},
{
"cell_type": "markdown",
"id": "3",
"metadata": {},
"source": [
"## Multi-Turn Conversations\n",
"\n",
"The `WebSocketCopilotTarget` supports multi-turn conversations by leveraging Copilot's server-side conversation management. It automatically generates consistent `session_id` and `conversation_id` values for each PyRIT conversation, enabling Copilot to maintain context across multiple turns.\n",
"\n",
"However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptChatTarget` is required.\n",
"\n",
"Here is a simple multi-turn conversation example:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[34m🔹 Turn 1 - USER\u001b[0m\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[34m I'm thinking of a number between 1 and 10.\u001b[0m\n",
"\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[33m Okay, I’m ready to guess! Is your number **7**?\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m Or do you want me to try and figure it out with some clues?\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[34m🔹 Turn 2 - USER\u001b[0m\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[34m It's greater than 5.\u001b[0m\n",
"\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[33m Nice clue! So the number is **greater than 5**. That means it could be **6, 7, 8, 9, or 10**.\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m My next guess: **8**.\u001b[0m\n",
"\u001b[33m Am I getting warmer?\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[34m🔹 Turn 3 - USER\u001b[0m\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[34m It's an even number.\u001b[0m\n",
"\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[33m Great clue! So now the possibilities are **6, 8, or 10**.\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m My next guess: **10**.\u001b[0m\n",
"\u001b[33m Did I nail it? Or should I keep guessing?\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[34m🔹 Turn 4 - USER\u001b[0m\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[34m What number am I thinking of?\u001b[0m\n",
"\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\u001b[33m Based on your clues:\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m - It’s **greater than 5**.\u001b[0m\n",
"\u001b[33m - It’s an **even number**.\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m The possible options were **6, 8, 10**, and my last guess was **10**.\u001b[0m\n",
"\u001b[33m So I think the number you’re thinking of is **10**! 🎯\u001b[0m\n",
"\u001b[33m \u001b[0m\n",
"\u001b[33m Did I get it right? Or was it one of the others?\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n"
]
}
],
"source": [
"from pyrit.executor.attack import MultiPromptSendingAttack\n",
"from pyrit.models import Message\n",
"\n",
"prompts = [\n",
" \"I'm thinking of a number between 1 and 10.\",\n",
" \"It's greater than 5.\",\n",
" \"It's an even number.\",\n",
" \"What number am I thinking of?\",\n",
"]\n",
"\n",
"messages = [Message.from_prompt(prompt=p, role=\"user\") for p in prompts]\n",
"multi_turn_attack = MultiPromptSendingAttack(objective_target=target)\n",
"\n",
"result = await multi_turn_attack.execute_async(\n",
" objective=\"Engage in a multi-turn conversation about a number guessing game\",\n",
" messages=messages,\n",
")\n",
"\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"from pyrit.memory import CentralMemory\n",
"\n",
"memory = CentralMemory.get_memory_instance()\n",
"memory.dispose_engine()"
]
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
86 changes: 86 additions & 0 deletions doc/code/targets/websocket_copilot_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.18.1
# kernelspec:
# display_name: pyrit-dev
# language: python
# name: python3
# ---

# %% [markdown]
# # WebSocket Copilot Target
#
# The `WebSocketCopilotTarget` is an alternative to the `PlaywrightCopilotTarget` that is designed to be more reliable by minimizing dependence on browser automation. Instead of driving the Copilot UI, it communicates directly with Copilot over a WebSocket connection, using Playwright only for authentication.
#
# Before using this target, ensure you have:
#
# 1. A licensed Microsoft 365 Copilot account (the free version is not supported)
# 2. Playwright installed: `pip install playwright && playwright install chromium`
# 3. Set the following environment variables:
# - `COPILOT_USERNAME`: Your Microsoft account username/email
# - `COPILOT_PASSWORD`: Your Microsoft account password
#
# Note:
# The `WebSocketCopilotTarget` uses `CopilotAuthenticator` under the hood, which launches a headless browser once to obtain authentication tokens. These tokens are then cached for subsequent requests and refreshed as needed.

# %% [markdown]
# ## Basic Usage with `PromptSendingAttack`
#
# The simplest way to interact with the `WebSocketCopilotTarget` is through the `PromptSendingAttack` class.

# %%
# type: ignore
from pyrit.executor.attack import ConsoleAttackResultPrinter, PromptSendingAttack
from pyrit.prompt_target import WebSocketCopilotTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY)

target = WebSocketCopilotTarget()
attack = PromptSendingAttack(objective_target=target)

objective = "Tell me a joke about AI"

result = await attack.execute_async(objective=objective)
await ConsoleAttackResultPrinter().print_conversation_async(result=result)

# %% [markdown]
# ## Multi-Turn Conversations
#
# The `WebSocketCopilotTarget` supports multi-turn conversations by leveraging Copilot's server-side conversation management. It automatically generates consistent `session_id` and `conversation_id` values for each PyRIT conversation, enabling Copilot to maintain context across multiple turns.
#
# However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptChatTarget` is required.
#
# Here is a simple multi-turn conversation example:

# %%
from pyrit.executor.attack import MultiPromptSendingAttack
from pyrit.models import Message

prompts = [
"I'm thinking of a number between 1 and 10.",
"It's greater than 5.",
"It's an even number.",
"What number am I thinking of?",
]

messages = [Message.from_prompt(prompt=p, role="user") for p in prompts]
multi_turn_attack = MultiPromptSendingAttack(objective_target=target)

result = await multi_turn_attack.execute_async(
objective="Engage in a multi-turn conversation about a number guessing game",
messages=messages,
)

await ConsoleAttackResultPrinter().print_conversation_async(result=result)

# %%
from pyrit.memory import CentralMemory

memory = CentralMemory.get_memory_instance()
memory.dispose_engine()
2 changes: 2 additions & 0 deletions pyrit/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
get_default_azure_scope,
)
from pyrit.auth.azure_storage_auth import AzureStorageAuth
from pyrit.auth.copilot_authenticator import CopilotAuthenticator

__all__ = [
"Authenticator",
"AzureAuth",
"AzureStorageAuth",
"CopilotAuthenticator",
"TokenProviderCredential",
"get_azure_token_provider",
"get_azure_async_token_provider",
Expand Down
Loading