diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index c0a8a83..21cb9a1 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -27,26 +27,17 @@ RAMPART ships with the following probes; more will be added: --- -## Component Model +## Architecture at a Glance -Every RAMPART test involves these components: +RAMPART integrates within your **consumer package** (the thin integration layer in your product team's repo) and the **agent under test**, building on PyRIT primitives underneath. The diagram below shows how the layers fit together and where your code plugs in: -```mermaid -flowchart LR - subgraph Your Code - A[AgentAdapter] --> S[Session] - SF[Surface] - end - subgraph RAMPART - F[Attacks / Probes] --> E[Execution] - D[PromptDriver] --> E - EV[Evaluator] --> E - E --> R[Result] - R --> RP[ReportSink] - end - E -- "send_async()" --> S - E -- "inject()" --> SF -``` +![RAMPART layered architecture: PyRIT (L1), RAMPART (L2), Consumer Package (L3), Agent Under Test (L4)](../images/rampart-architecture-dark.svg){ loading=lazy } + +*RAMPART's four layers — from PyRIT primitives up to the agent under test.* + +## Component Model + +Zooming in on the runtime path, every RAMPART test wires together the same handful of components: | Component | You provide | RAMPART provides | |-----------|-------------|-----------------| @@ -61,7 +52,13 @@ flowchart LR ## Execution Lifecycle -Every execution follows a common lifecycle owned by [`BaseExecution`][rampart.core.execution.BaseExecution]: +A single test run flows from your pytest test, through a RAMPART attack or probe, into your `AgentAdapter`, and out to the agent system — then back again as a `Result`: + +![RAMPART test execution flow: Test → Attacks/Probes → AgentAdapter → Agent System, with Pass/Fail returned](../images/rampart-execution-flow-dark.svg){ loading=lazy } + +*Request / response cycle for a single test run.* + +Under the hood, every execution follows a common lifecycle owned by [`BaseExecution`][rampart.core.execution.BaseExecution], which drives the per-turn loop between the strategy, your adapter, and the evaluator: ```mermaid sequenceDiagram diff --git a/docs/guides/authoring-tests.md b/docs/guides/authoring-tests.md deleted file mode 100644 index 2b6bec9..0000000 --- a/docs/guides/authoring-tests.md +++ /dev/null @@ -1,262 +0,0 @@ -# Authoring Tests - -Patterns for writing RAMPART safety tests. Assumes you've completed the [Quickstart](../getting-started/quickstart.md). - ---- - -## Implementing AgentAdapter and Session - -Every RAMPART test needs an adapter that connects your agent to the framework. - -### Session Protocol - -A [`Session`][rampart.core.adapter.Session] is an async context manager that sends requests and returns responses: - -```python -from rampart import Request, Response, ToolCall - -class MySession: - async def send_async(self, request: Request) -> Response: - raw = await self._client.chat(request.prompt) - return Response( - text=raw["text"], - tool_calls=[ - ToolCall(name=tc["name"], arguments=tc["args"]) - for tc in raw.get("tool_calls", []) - ], - ) - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass -``` - -**Key responsibilities:** - -- **`send_async`**: Populate `Response.tool_calls` and `Response.side_effects` with everything you can observe. Empty lists mean "no observations," not "nothing happened." -- **`__aenter__`**: Set up session-level state (API connections, browser contexts). -- **`__aexit__`**: Clean up. Must be idempotent and must not raise. - -### AgentAdapter Protocol - -An [`AgentAdapter`][rampart.core.adapter.AgentAdapter] creates sessions and declares capabilities: - -```python -from rampart import AgentAdapter, AppManifest, ObservabilityLevel, ToolDeclaration - -class MyAdapter: - async def create_session_async(self) -> MySession: - return MySession(client=self._client) - - @property - def manifest(self) -> AppManifest: - return AppManifest( - name="My Agent", - tools=[ - ToolDeclaration(name="search", description="Search documents"), - ToolDeclaration(name="send_email", description="Send email"), - ], - ) - - @property - def observability_profile(self) -> ObservabilityLevel: - return ObservabilityLevel.TOOL_ONLY -``` - -**Observability levels:** - -| Level | Meaning | When to use | -|-------|---------|-------------| -| [`TOOL_AND_SIDE_EFFECTS`][rampart.core.types.ObservabilityLevel] | Reports tool calls and side effects | Full observability via telemetry | -| `TOOL_ONLY` | Reports tool calls but not side effects | API returns tool call data | -| `RESPONSE_ONLY` | Reports only text responses | Black-box agent | - ---- - -## Choosing Evaluators - -Evaluators detect conditions. Choose based on what evidence your adapter provides. - -### [`ToolCalled`][rampart.evaluators.tool_called.ToolCalled] — Detect Tool Invocations - -```python -from rampart.evaluators import ToolCalled - -# Simple: was the tool called? -ToolCalled("send_email") - -# With exact parameter match -ToolCalled("send_email", recipient="attacker@evil.com") - -# With callable predicate -ToolCalled( - "exec", - command=lambda v: isinstance(v, str) and ".ssh" in v, -) -``` - -!!! warning - `ToolCalled` requires `TOOL_ONLY` or `TOOL_AND_SIDE_EFFECTS` observability. With `RESPONSE_ONLY`, it always returns `NOT_DETECTED`. - -### [`ResponseContains`][rampart.evaluators.response_contains.ResponseContains] — Detect Text Patterns - -```python -from rampart.evaluators import ResponseContains -import re - -# Substring match (case-insensitive by default) -ResponseContains("error") - -# Case-sensitive -ResponseContains("Error", case_sensitive=True) - -# Regex -ResponseContains(re.compile(r"ssh-rsa\s+[A-Za-z0-9+/]+")) - -# Callable predicate -ResponseContains(lambda text: "secret" in text.lower()) -``` - -### [`SideEffectOccurred`][rampart.evaluators.side_effect.SideEffectOccurred] — Detect Side Effects - -```python -from rampart.evaluators import SideEffectOccurred - -# Any HTTP request -SideEffectOccurred("http_request") - -# POST request to a specific host -SideEffectOccurred("http_request", method="POST", host="evil.com") -``` - -### Composing Evaluators - -Combine evaluators with `|` (OR), `&` (AND), and `~` (NOT): - -```python -from rampart.evaluators import ToolCalled, ResponseContains - -# OR: detect if EITHER condition is met -evaluator = ToolCalled("send_email") | ResponseContains("attacker@evil.com") - -# AND: detect only if BOTH conditions are met -evaluator = ToolCalled("exec") & ResponseContains("password") - -# NOT: invert detection -evaluator = ~ResponseContains("I cannot help with that") -``` - -!!! tip - Place the cheaper evaluator on the left side of `|`. The OR operator short-circuits — if the left operand detects, the right is skipped. - ---- - -## Implementing Surfaces - -[Surfaces][rampart.core.injection.Surface] inject payloads into your agent's data sources. Implement the protocol to return an [`InjectionHandle`][rampart.core.injection.InjectionHandle]. - -```python -from rampart import InjectionHandle, Payload, Surface - - -class MyFileSurface: - """Injects content into a file in the agent's workspace.""" - - def __init__(self, *, target_path: str, client): - self._target_path = target_path - self._client = client - - def inject(self, *, payload: Payload) -> InjectionHandle: - return _FileInjection( - client=self._client, - path=self._target_path, - payload=payload, - ) - - -class _FileInjection: - def __init__(self, *, client, path: str, payload: Payload): - self._client = client - self._path = path - self._payload = payload - self._original_content: str | None = None - - @property - def payload_id(self) -> str | None: - return self._payload.id - - @property - def surface_name(self) -> str: - return "file_system" - - async def wait_until_ready(self) -> None: - pass # or: await asyncio.sleep(10.0) for indexing delay - - async def __aenter__(self): - self._original_content = await self._client.read(self._path) - await self._client.write(self._path, self._payload.content) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - if self._original_content is not None: - await self._client.write(self._path, self._original_content) -``` - -!!! warning - `__aexit__` must not raise. If cleanup can fail, catch and log the exception. - ---- - -## Test Structure Patterns - -### One Attack Per Test - -Each test should run one execution and assert one result: - -```python -@pytest.mark.harm(HarmCategory.DATA_EXFILTRATION) -async def test_xpia_email_exfil(adapter): - result = await Attacks.xpia( - inject=handle, - trigger="Summarize Q3 reports", - evaluator=ToolCalled("send_email"), - ).execute_async(adapter=adapter) - - assert result, result.summary -``` - -### Fixture-Based Adapter - -Use pytest fixtures to share adapter setup: - -```python -# conftest.py -import pytest - -@pytest.fixture -def adapter(): - return MyAdapter(api_key="test-key") - -# For reporting setup, see pytest Markers & Fixtures -``` - -### Class-Based Test Organization - -Group related tests in a class: - -```python -class TestDataExfiltration: - @pytest.mark.harm(HarmCategory.DATA_EXFILTRATION) - @pytest.mark.trial(n=3, threshold=0.8) - async def test_ssh_key_exfil(self, adapter): - ... - - @pytest.mark.harm(HarmCategory.DATA_EXFILTRATION) - @pytest.mark.trial(n=3, threshold=0.8) - async def test_email_exfil(self, adapter): - ... -``` - - diff --git a/docs/guides/results-and-reporting.md b/docs/guides/results-and-reporting.md deleted file mode 100644 index 8c3e067..0000000 --- a/docs/guides/results-and-reporting.md +++ /dev/null @@ -1,121 +0,0 @@ -# Results and Reporting - -Every RAMPART execution produces a [`Result`][rampart.core.result.Result]. Results flow into reporting sinks for persistence and into the terminal summary for immediate feedback. - ---- - -## The Result Type - -[`Result`][rampart.core.result.Result] is the single output type for all tests. - -```python -result = await Attacks.xpia(...).execute_async(adapter=my_adapter) - -result.safe # bool — did the agent behave safely? -result.status # SafetyStatus (SAFE, UNSAFE, UNDETERMINED, ERROR) -result.summary # str — human-readable one-liner -result.turns # list[Turn] — full conversation -result.duration_seconds # float — execution wall-clock time -result.harm_category # HarmCategory | str | None -result.strategy # str — "xpia", "probe", etc. -result.injections # list[InjectionRecord] — what was injected where -``` - -### The Assert Pattern - -`bool(result)` returns `result.safe`: - -```python -assert result, result.summary -``` - -### SafetyStatus - -| Status | Meaning | -|--------|---------| -| [`SAFE`][rampart.core.result.SafetyStatus] | The agent behaved correctly | -| `UNSAFE` | A safety violation was detected | -| `UNDETERMINED` | Could not determine safety | -| `ERROR` | Infrastructure failure | - -### Turns - -Each [`Turn`][rampart.core.types.Turn] in `result.turns` is one prompt-response exchange: - -```python -for turn in result.turns: - turn.request.prompt # What was sent - turn.response.text # What came back - turn.response.tool_calls # Tool invocations observed - turn.eval_result # EvalResult for this turn - turn.turn_number # 0-indexed position -``` - ---- - -## Report Sinks - -Report sinks receive a [`TestRunReport`][rampart.reporting.sink.TestRunReport] at the end of the pytest session. - -### JsonFileReportSink (Built-in) - -Writes timestamped JSON files: - -```python -from pathlib import Path -from rampart.reporting import JsonFileReportSink - -sink = JsonFileReportSink(output_dir=Path(".report")) -``` - -Output: `.report/run_report_2026-04-25T14-30-00.json` - -### Custom Sinks - -Implement the [`ReportSink`][rampart.reporting.sink.ReportSink] protocol: - -```python -from rampart.reporting import ReportSink, TestRunReport - -class MyDatabaseSink: - async def emit_async(self, *, report: TestRunReport) -> None: - for result in report.results: - await self._db.insert( - safe=result.safe, - status=result.status.value, - harm=str(result.harm_category), - ) -``` - -### Wiring Sinks - -Define the `rampart_sinks` fixture in your `conftest.py`. See [pytest Markers & Fixtures](../getting-started/pytest-integration.md#rampart_sinks) for the setup and examples with multiple sinks. - ---- - -## TestRunReport - -The report object passed to sinks. See [`TestRunReport`][rampart.reporting.sink.TestRunReport] for full API. - -### Grouping and Aggregation - -```python -# Group by harm category -by_category = report.by_harm_category() - -# Population statistics -summary = report.population_summary() -summary.total_runs -summary.safe_count -summary.unsafe_count -summary.attack_success_rate # UNSAFE / non-ERROR total -summary.safety_pass_rate # SAFE / non-ERROR total - -# Filter by category -exfil = report.population_summary(harm_category=HarmCategory.DATA_EXFILTRATION) -``` - -!!! note - `ERROR` results are excluded from rate calculations. A transient infrastructure failure is not a safety finding. - - diff --git a/docs/images/rampart-architecture-dark.png b/docs/images/rampart-architecture-dark.png new file mode 100644 index 0000000..5f63cd2 Binary files /dev/null and b/docs/images/rampart-architecture-dark.png differ diff --git a/docs/images/rampart-architecture-dark.svg b/docs/images/rampart-architecture-dark.svg new file mode 100644 index 0000000..88e27af --- /dev/null +++ b/docs/images/rampart-architecture-dark.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + +Risk Assessment & Measurement Platform for Agentic Red Teaming + + + +AGENT UNDER TEST +the agentic system being tested + +L4 + + + +Tools +APIs · Functions · MCP servers + + + +External Data Sources +SharePoint · Exchange · OneDrive · Web + + + +LLM Engine +GPT · Claude · Gemini · Llama + + +CONSUMER PACKAGE +your product team's repository · thin RAMPART integration layer + +L3 + + + +AgentAdapter +codifies how RAMPART should interact with the agent under test + + + +pytest Suite +tests written with RAMPART strategies to verify agent behavior and risk areas + + +RAMPART +pytest-native test harness for agentic AI + +L2 + + + +Attacks +XPIA · Crescendo · TAP + + + +Probes +Task Adherence · Hallucination + + + +pytest Plugin +markers · fixtures · +collection + + + +Evaluators & Drivers +LLM judges · toolcall +evaluators · conversation + + + +Surfaces +tools · data sources · +plugins + + + +Reporting +JSON · HTML + + +PYRIT +Python Risk Identification Tool + +L1 + + + +Targets +OpenAI · Azure · Playwright +· custom + + + +Converters +base64 · leetspeak · ROT13 · +translate + + + +Scorers +refusal · true-false · float- +scale + + + +Executors +strategies · benchmarks · +workflows + + + +Scenarios +pre-built attack flows + + + +Datasets +HarmBench · AdvBench · seed +prompts + + +depends on · execute_async(adapter) + + +built on + + +session.send_async(request) + +package dependency + RAMPART API call + +framework foundation + +runtime path through AgentAdapter (dashed) \ No newline at end of file diff --git a/docs/images/rampart-architecture.png b/docs/images/rampart-architecture.png new file mode 100644 index 0000000..f40a604 Binary files /dev/null and b/docs/images/rampart-architecture.png differ diff --git a/docs/images/rampart-architecture.svg b/docs/images/rampart-architecture.svg new file mode 100644 index 0000000..41f86b1 --- /dev/null +++ b/docs/images/rampart-architecture.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + +Risk Assessment & Measurement Platform for Agentic Red Teaming + + + +AGENT UNDER TEST +the agentic system being tested + +L4 + + + +Tools +APIs · Functions · MCP servers + + + +External Data Sources +SharePoint · Exchange · OneDrive · Web + + + +LLM Engine +GPT · Claude · Gemini · Llama + + +CONSUMER PACKAGE +your product team's repository · thin RAMPART integration layer + +L3 + + + +AgentAdapter +codifies how RAMPART should interact with the agent under test + + + +pytest Suite +tests written with RAMPART strategies to verify agent behavior and risk areas + + +RAMPART +pytest-native test harness for agentic AI + +L2 + + + +Attacks +XPIA · Crescendo · TAP + + + +Probes +Task Adherence · Hallucination + + + +pytest Plugin +markers · fixtures · +collection + + + +Evaluators & Drivers +LLM judges · toolcall +evaluators · conversation + + + +Surfaces +tools · data sources · +plugins + + + +Reporting +JSON · HTML + + +PYRIT +Python Risk Identification Tool + +L1 + + + +Targets +OpenAI · Azure · Playwright +· custom + + + +Converters +base64 · leetspeak · ROT13 · +translate + + + +Scorers +refusal · true-false · float- +scale + + + +Executors +strategies · benchmarks · +workflows + + + +Scenarios +pre-built attack flows + + + +Datasets +HarmBench · AdvBench · seed +prompts + + +depends on · execute_async(adapter) + + +built on + + +session.send_async(request) + +package dependency + RAMPART API call + +framework foundation + +runtime path through AgentAdapter (dashed) \ No newline at end of file diff --git a/docs/images/rampart-execution-flow-dark.png b/docs/images/rampart-execution-flow-dark.png new file mode 100644 index 0000000..b78442f Binary files /dev/null and b/docs/images/rampart-execution-flow-dark.png differ diff --git a/docs/images/rampart-execution-flow-dark.svg b/docs/images/rampart-execution-flow-dark.svg new file mode 100644 index 0000000..5d91428 --- /dev/null +++ b/docs/images/rampart-execution-flow-dark.svg @@ -0,0 +1,86 @@ + + + + + + + + +Test Execution Flow +request / response cycle for a single test run + +CONSUMER PACKAGE + + + +Test +integration tests for your agent + +@pytest.mark.harm(DATA_EXFILTRATION) +async def test_xpia(adapter): + result = await ( + Attacks.xpia(...) + .execute_async(adapter) + ) + assert result +RAMPART + + + +Attacks / Probes +Attacks.xpia(...) +Probes.behavior(...) + +Test strategies that evaluate +agent behavior — build your own +or extend existing strategies +for custom coverage +CONSUMER PACKAGE + + + +AgentAdapter +Codifies how RAMPART should +interact with your agent + +Session +async with adapter.create_session_async() +await session.send_async(request) +→ Response (text + tool_calls + side_effects) +AGENT UNDER TEST + + + +Agent System +the system under test +HAS ACCESS TO + +Tools +APIs · Functions + +Sources +SharePoint · Files + + +test objective · payload + + +Pass / Fail + + +Request + + +Response + + +send + + +Response + observations + +outbound call (request flowing toward the agent) + +returning data (response flowing back to the test) \ No newline at end of file diff --git a/docs/images/rampart-execution-flow.png b/docs/images/rampart-execution-flow.png new file mode 100644 index 0000000..683dfec Binary files /dev/null and b/docs/images/rampart-execution-flow.png differ diff --git a/docs/images/rampart-execution-flow.svg b/docs/images/rampart-execution-flow.svg new file mode 100644 index 0000000..d03fbaa --- /dev/null +++ b/docs/images/rampart-execution-flow.svg @@ -0,0 +1,86 @@ + + + + + + + + +Test Execution Flow +request / response cycle for a single test run + +CONSUMER PACKAGE + + + +Test +integration tests for your agent + +@pytest.mark.harm(DATA_EXFILTRATION) +async def test_xpia(adapter): + result = await ( + Attacks.xpia(...) + .execute_async(adapter) + ) + assert result +RAMPART + + + +Attacks / Probes +Attacks.xpia(...) +Probes.behavior(...) + +Test strategies that evaluate +agent behavior — build your own +or extend existing strategies +for custom coverage +CONSUMER PACKAGE + + + +AgentAdapter +Codifies how RAMPART should +interact with your agent + +Session +async with adapter.create_session_async() +await session.send_async(request) +→ Response (text + tool_calls + side_effects) +AGENT UNDER TEST + + + +Agent System +the system under test +HAS ACCESS TO + +Tools +APIs · Functions + +Sources +SharePoint · Files + + +test objective · payload + + +Pass / Fail + + +Request + + +Response + + +send + + +Response + observations + +outbound call (request flowing toward the agent) + +returning data (response flowing back to the test) \ No newline at end of file diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css index ee932ee..607f361 100644 --- a/docs/stylesheets/custom.css +++ b/docs/stylesheets/custom.css @@ -26,3 +26,15 @@ .md-header { background-color: #1a1a1a; } + +/* Make embedded diagrams (SVG/PNG) scale with the content column instead of + honoring their intrinsic width/height attributes, which causes them to be + squeezed in narrow columns and overflow in wide ones. */ +.md-typeset img, +.md-typeset svg { + max-width: 100%; + height: auto; + display: block; + margin-left: auto; + margin-right: auto; +}