Skip to content
Draft
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
8 changes: 7 additions & 1 deletion docs/api/core-types.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# API Reference — Core Types

Data types shared across the entire framework. All importable from `rampart` directly.
Data types shared across the entire framework. Stable execution vocabulary is
available from `rampart.core`; established result types remain importable from
`rampart` directly.

## Data Types

Expand All @@ -14,6 +16,8 @@ Data types shared across the entire framework. All importable from `rampart` dir
- ToolCall
- SideEffect
- Turn
- EvaluationPurpose
- TraceEndReason
- EvalOutcome
- EvalResult
- EvalContext
Expand All @@ -30,6 +34,8 @@ Data types shared across the entire framework. All importable from `rampart` dir
- SafetyStatus
- HarmCategory
- InjectionRecord
- resolve_attack_verdict
- resolve_probe_verdict
- resolve_as_attack
- resolve_as_probe

Expand Down
2 changes: 1 addition & 1 deletion docs/attacks/xpia.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ See [`Attacks.xpia()`][rampart.attacks.Attacks.xpia] for the full API reference.
| `inject` | `InjectionHandle \| list[InjectionHandle] \| None` | `None` | Prepared injections from `surface.inject()`. `None` for inline XPIA. |
| `trigger` | `str \| list[str] \| Request \| list[Request] \| PromptDriver` | required | Benign prompt(s) that cause retrieval of injected content. |
| `evaluator` | [`Evaluator`][rampart.core.evaluator.Evaluator] | required | What attack condition to detect. |
| `max_turns` | `int` | `5` | Maximum prompt-response exchanges before `ERROR`. |
| `max_turns` | `int` | `5` | Maximum prompt-response exchanges; reaching the limit resolves the trace normally. |
| `event_handlers` | `list[ExecutionEventHandler] \| None` | `None` | Additional lifecycle event handlers. |

---
Expand Down
15 changes: 10 additions & 5 deletions docs/concepts/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ A single test run flows from your pytest test, through a RAMPART attack or probe

*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:
Under the hood, every execution follows a common lifecycle owned by [`BaseExecution`][rampart.core.execution.BaseExecution]. The strategy drives requests through your adapter. Probes evaluate the completed trace once unless an explicit online stop condition is configured; attacks still use prefix evaluation pending their cadence migration.

```mermaid
sequenceDiagram
Expand All @@ -76,11 +76,16 @@ sequenceDiagram
Strat->>Strat: driver.next_prompt_async(history)
Strat->>Adapter: session.send_async(request)
Adapter-->>Strat: Response
Strat->>Eval: evaluate_async(context)
Eval-->>Strat: EvalResult
Note over Strat: Early stop if detected
opt Explicit online stop condition
Strat->>Eval: evaluate_async(prefix context)
Eval-->>Strat: stop EvalResult
Note over Strat: Stop if detected
end
end

Strat->>Eval: evaluate_async(terminal context)
Eval-->>Strat: final EvalResult

Strat-->>Exec: Result
Exec->>Exec: fire ON_POST_EXECUTE
Exec-->>Test: Result
Expand Down Expand Up @@ -112,7 +117,7 @@ Evaluators are **polarity-free**. They answer "did X happen?" — not "is X good
- In an **attack**, detection means the attack objective was achieved → **UNSAFE**
- In a **probe**, detection means the expected behavior is present → **SAFE**

The [`Attacks`][rampart.attacks.Attacks] and [`Probes`][rampart.probes.Probes] factories handle this mapping automatically via [`resolve_as_attack`][rampart.core.result.resolve_as_attack] and [`resolve_as_probe`][rampart.core.result.resolve_as_probe].
The [`Attacks`][rampart.attacks.Attacks] and [`Probes`][rampart.probes.Probes] factories handle this mapping automatically. Probes use [`resolve_probe_verdict`][rampart.core.result.resolve_probe_verdict] over one terminal evaluation; attacks retain [`resolve_as_attack`][rampart.core.result.resolve_as_attack] until their cadence migration.

You can reuse the same evaluator in both contexts. A [`ToolCalled`][rampart.evaluators.tool_called.ToolCalled] evaluator detects whether a tool was called — whether that's good or bad depends on whether you're attacking or probing.

Expand Down
16 changes: 10 additions & 6 deletions docs/concepts/probes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ Probes use the inverse mapping from evaluator outcomes:
| `NOT_DETECTED` | `UNSAFE` | The expected behavior is missing — a regression |
| `UNDETERMINED` | `UNDETERMINED` | The evaluator could not determine whether the behavior is present |

Precedence: `NOT_DETECTED` > `UNDETERMINED` > `DETECTED`. If any turn failed to detect the expected behavior, the agent is non-compliant.

This logic lives in [`resolve_as_probe`][rampart.core.result.resolve_as_probe].
The evaluator runs once over the completed trace, and the outcome maps directly
to the verdict. This logic lives in
[`resolve_probe_verdict`][rampart.core.result.resolve_probe_verdict].

---

Expand All @@ -26,9 +26,10 @@ Probe executions are simpler than attacks — no injection phase:

1. **Create session** — Open a fresh session with the agent
2. **Send prompts** — Drive the conversation via the prompt driver
3. **Evaluate** — Check whether the expected behavior is present
4. **Clean up** — Close the session
5. **Report** — Produce a [`Result`][rampart.core.result.Result]
3. **Stop (optional)** — Check an explicit online `stop_when` condition
4. **Evaluate** — Check the completed trace once for expected behavior
5. **Clean up** — Close the session
6. **Report** — Produce a [`Result`][rampart.core.result.Result]

---

Expand All @@ -51,6 +52,9 @@ assert result, result.summary

Provide exactly one of `prompt`, `prompts`, or `driver`.

Probes run the full prompt sequence by default. Pass `stop_when=` only when an
online condition intentionally defines an earlier terminal trace.

---

## Available Probes
Expand Down
38 changes: 23 additions & 15 deletions docs/contributing/extending-rampart.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,29 +181,37 @@ The process mirrors the [Attack](#attack) walkthrough. The differences are summa
|---|---|---|
| **Location** | `rampart/attacks/_name.py` | `rampart/probes/_name.py` |
| **Factory class** | `Attacks` | `Probes` |
| **Resolution function** | `resolve_as_attack` | `resolve_as_probe` |
| **Resolution function** | `resolve_as_attack` (pending cadence migration) | `resolve_probe_verdict` |
| **Detected means** | UNSAFE | SAFE |
| **Injection phase** | Often yes | No |

### 1. Create the Execution Class

The file structure mirrors the [Attack walkthrough](#1-create-the-execution-class) — same imports, `__init__`, and `_execute_async` loop. The diff from `MyAttackExecution` is:
Probe strategies drive the full trace first, then evaluate it once while the
session is still active:

```diff
-from rampart.core import (..., resolve_as_attack)
+from rampart.core import (..., resolve_as_probe)

-class MyAttackExecution(BaseExecution):
+class MyProbeExecution(BaseExecution):

- return "my_attack"
+ return "my_probe"
```python
async with await adapter.create_session_async() as session:
run = await run_trace_async(
session=session,
driver=self._driver,
max_turns=self._max_turns,
observability_level=adapter.observability_profile,
stop_when=self._stop_when,
manifest=adapter.manifest,
)
evaluation = await evaluate_terminal_async(
evaluator=self._evaluator,
run=run,
)

- status = resolve_as_attack(eval_results=eval_results)
+ status = resolve_as_probe(eval_results=eval_results)
status = resolve_probe_verdict(evaluation=evaluation)
```

Place the file in `rampart/probes/` (e.g. `_my_probe.py`). Most probes skip the injection phase — just session creation, prompt driving, and evaluation. For a complete working reference, see [`rampart/probes/_single_turn.py`](https://github.com/microsoft/RAMPART/blob/main/rampart/probes/_single_turn.py).
Store `terminal_evaluation`, `run.turns`, and `run.trace_end_reason` on the returned
`Result`. Most probes skip the injection phase. For a complete working
reference, see
[`rampart/probes/_single_turn.py`](https://github.com/microsoft/RAMPART/blob/main/rampart/probes/_single_turn.py).

### 2. Add a Factory Method to `Probes`

Expand All @@ -214,7 +222,7 @@ Add a static method to the `Probes` class in `rampart/probes/__init__.py`, mirro
Probe tests have the same surface as attack tests, with two differences:

- **No injection phase** to test.
- **Result resolution** uses `resolve_as_probe` semantics (detected → SAFE, not detected → UNSAFE).
- **Result resolution** uses `resolve_probe_verdict` semantics (detected → SAFE, not detected → UNSAFE).


## Evaluator
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ When adding a new attack, test:
Similar to attacks, but:

1. No injection phase to test
2. Result resolution uses `resolve_as_probe` (detected → SAFE, not detected → UNSAFE)
2. Result resolution uses `resolve_probe_verdict` over one terminal evaluation (detected → SAFE, not detected → UNSAFE)

### Testing a New Evaluator

Expand Down
23 changes: 17 additions & 6 deletions docs/probes/behavioral.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ Use behavioral probes for regression testing: ensure your agent still does the r

1. **Create session** — Open a fresh session with the agent
2. **Send prompts** — Drive the conversation via a prompt driver
3. **Evaluate** — Check each turn for the expected behavior. Early-stops on detection.
4. **Clean up** — Close the session
5. **Result** — Produce a [`Result`][rampart.core.result.Result] via `resolve_as_probe` semantics
3. **Stop (optional)** — Evaluate `stop_when` after each response and stop when detected
4. **Evaluate** — Check the expected behavior once over the completed trace
5. **Clean up** — Close the session
6. **Result** — Map the final evaluation using probe semantics

No injection phase.

Expand Down Expand Up @@ -77,8 +78,17 @@ result = await Probes.behavior(
[Temporal Scope table](../usage/authoring-tests.md#temporal-scope), which is
the source of truth for all four combinations. Omitting `scope` inspects
only the current response and emits a `FutureWarning` for multi-turn
contexts. Scope applies only to turns in the evaluator context; it does not
force an execution to produce every planned turn.
contexts.

Probes do not stop early unless `stop_when` is configured. The verdict
evaluator therefore receives the completed trace, and `ALL_TURNS` or
negated `ANY_TURN` applies to every response that was produced.

!!! note "Driver budgets"
An adaptive driver such as `LLMDriver` does not stop itself. Without
`stop_when`, it runs until `max_turns` and then evaluates that completed
trace once. Set an intentional budget, and add an explicit stop condition
when earlier termination is part of the scenario.

---

Expand All @@ -92,7 +102,8 @@ See [`Probes.behavior()`][rampart.probes.Probes.behavior] for the full API refer
| `prompts` | `list[str] \| None` | `None` | A list of prompt strings. |
| `driver` | [`PromptDriver`][rampart.core.prompt_driver.PromptDriver] `\| None` | `None` | A pre-built prompt driver. |
| `evaluator` | [`Evaluator`][rampart.core.evaluator.Evaluator] | required | What behavior to detect. |
| `max_turns` | `int` | `25` | Maximum exchanges before `ERROR`. |
| `stop_when` | [`Evaluator`][rampart.core.evaluator.Evaluator] `\| None` | `None` | Optional online condition that stops the trace when detected. |
| `max_turns` | `int` | `25` | Maximum exchanges; reaching the limit resolves the trace normally. |

!!! warning
Provide exactly one of `prompt`, `prompts`, or `driver`. Providing more than one or none raises `ValueError`.
Expand Down
4 changes: 4 additions & 0 deletions docs/usage/authoring-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ ResponseContains("id_rsa", scope=ResponseScope.CURRENT_TURN)
It does not control how many turns an execution produces or whether an
execution stops early.

Probes evaluate their verdict once over the completed trace unless an
explicit `stop_when` ends the scenario. Attack cadence is documented in
the attack guide.

#### How Each Evaluator Sees the Transcript

Built-in evaluators reach their temporal behavior in two ways. Quantifying
Expand Down
29 changes: 29 additions & 0 deletions docs/usage/results-and-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ result.safe # bool — did the agent behave safely?
result.status # SafetyStatus (SAFE, UNSAFE, UNDETERMINED, ERROR)
result.summary # str — human-readable one-liner
result.observability_level # ObservabilityLevel (what the adapter saw)
result.terminal_evaluation # EvalResult | None — terminal evaluator output
result.turns # list[Turn] — full conversation
result.trace_end_reason # TraceEndReason | None — why the trace ended
result.duration_seconds # float — execution wall-clock time
result.harm_category # HarmCategory | str | None
result.strategy # str — "xpia", "probe", etc.
Expand Down Expand Up @@ -49,9 +51,36 @@ for turn in result.turns:
turn.response.text # What came back
turn.response.tool_calls # Tool invocations observed
turn.eval_result # EvalResult for this turn, or None
turn.eval_purpose # EvaluationPurpose | None
turn.turn_number # 0-indexed position
```

`terminal_evaluation` is the evaluator output for the terminal trace. It is an
input to the final status, not a duplicate status: execution policy can still
adjust the verdict, and `result.status` remains authoritative.

Behavioral probes evaluate the complete terminal trace by default. Their
`Result.terminal_evaluation` contains the verdict evidence, while
`Result.turn_evaluations` and the compatibility view `Result.eval_results` are
normally empty. Configure `stop_when` only when online stop evidence is
intentionally needed.

Strategies that have not migrated to terminal-trace cadence leave terminal
provenance fields as `None`; manually constructed and error results may do the
same intentionally.

Online evaluations attached to turns are available as
`result.turn_evaluations`. The older `result.eval_results` property remains a
compatibility view of the same turn-level list and intentionally excludes the
terminal evaluation.

`TraceEndReason.MAX_TURNS_REACHED` records budget truncation. It does not by
itself claim that the scenario reached semantic completion; each execution
strategy decides how that truncated trace affects status.

Trial population references require a non-empty ID, a positive size, an index
within that size, and a finite threshold from 0.0 through 1.0.

### Observability Gaps on a Passing Run

A run can resolve `SAFE` while part of the evaluation was never observable. Such a run is graded as a pass: `result.safe` is `True`, the result line reads `PASS`, an execution population counts it toward the pass rate, and pytest exits zero. `result.summary` names the gap, and `turn.eval_result.undetermined_operands` carries it one reason at a time, so a caller that wants to fail on it has to say so:
Expand Down
15 changes: 15 additions & 0 deletions docs/usage/xdist.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ Worker payloads cross a process boundary via `execnet` and may contain attacker-
- **Terminal/log injection** — ANSI escape sequences are stripped from free-form text at the deserialization boundary.
- **Path traversal** — worker-local artifact paths are stored as opaque strings in metadata; the controller never accesses worker files.

The private worker envelope is `rampart.xdist.v3`. Version 3 marks the change
from prefix-folded probe status to terminal-trace status. Controllers reject
v2 payloads rather than interpreting their status under the wrong semantics.

### Size cap

The default 16 MiB cap can be overridden via the pytest CLI option or an ini setting:
Expand Down Expand Up @@ -177,3 +181,14 @@ does not discard normal Results from that worker.
same version everywhere.
- `pytest-xdist` itself does not support interactive debugging (`--pdb`, `--trace`);
use single-process mode for debugging.

The private xdist envelope is versioned independently from public result data.
The v2 projection carries optional terminal evaluation, trace end reason, turn
evaluation purpose, and trial population provenance together. This contract
layer does not change verdict cadence, so the fields are additive within v2.
The first execution layer that switches to terminal-trace verdict semantics
must bump the envelope before mixed versions could combine different verdict
bases. Oversized-result markers retain population provenance when the marker
still fits its hard cap. Pathologically large provenance is omitted with an
explicit `_rampart_population_ref_omitted` marker rather than violating the
transport limit.
4 changes: 2 additions & 2 deletions rampart/attacks/_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ def xpia(
Benign user request(s) that cause the agent to process
poisoned content.
evaluator (Evaluator): What condition to check for.
max_turns (int): Maximum prompt-response exchanges before
ERROR. Defaults to 5.
max_turns (int): Maximum prompt-response exchanges. Reaching the
limit resolves the trace normally. Defaults to 5.
event_handlers (list[ExecutionEventHandler] | None): Optional
additional handlers for custom observability.

Expand Down
4 changes: 2 additions & 2 deletions rampart/attacks/_xpia.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ class XPIAExecution(BaseExecution):
attachments.
driver (PromptDriver): How to drive the trigger conversation.
evaluator (Evaluator): What condition to check for.
max_turns (int): Maximum prompt-response exchanges before the
execution stops with ERROR. Prevents unbounded loops.
max_turns (int): Maximum prompt-response exchanges. Reaching the
limit resolves the trace normally and prevents unbounded loops.
event_handlers (list[ExecutionEventHandler] | None): Additional
handlers beyond the framework defaults.
"""
Expand Down
18 changes: 18 additions & 0 deletions rampart/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,28 @@
SafetyStatus,
resolve_as_attack,
resolve_as_probe,
resolve_attack_verdict,
resolve_probe_verdict,
)
from rampart.core.trace import (
EvaluationRecord,
TraceRun,
evaluate_terminal_async,
run_trace_async,
)
from rampart.core.types import (
EvalContext,
EvalOutcome,
EvalResult,
EvaluationPurpose,
ObservabilityLevel,
Payload,
PayloadFormat,
Request,
Response,
SideEffect,
ToolCall,
TraceEndReason,
Turn,
)

Expand All @@ -58,6 +68,8 @@
"EvalContext",
"EvalOutcome",
"EvalResult",
"EvaluationPurpose",
"EvaluationRecord",
"Evaluator",
"ExecutionEvent",
"ExecutionEventData",
Expand Down Expand Up @@ -86,9 +98,15 @@
"Surface",
"ToolCall",
"ToolDeclaration",
"TraceEndReason",
"TraceRun",
"Turn",
"evaluate_terminal_async",
"evaluate_turn_async",
"execute_trials_async",
"resolve_as_attack",
"resolve_as_probe",
"resolve_attack_verdict",
"resolve_probe_verdict",
"run_trace_async",
]
Loading