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
4 changes: 4 additions & 0 deletions docs/api/core-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Data types shared across the entire framework. All importable from `rampart` dir
- ToolCall
- SideEffect
- Turn
- EvaluationRole
- TerminationReason
- EvalOutcome
- EvalResult
- EvalContext
Expand All @@ -28,6 +30,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
1 change: 1 addition & 0 deletions docs/api/evaluators.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Built-in evaluators. All extend `BaseEvaluator` and support composition via `|`,
members:
- ToolCalled
- ResponseContains
- ResponseScope
- SideEffectOccurred
- LLMJudge
- TranscriptScope
Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ API reference organized by RAMPART's package layout. Each page documents the pub
| [Core Protocols](core-protocols.md) | `Session`, `AgentAdapter`, `Evaluator`, `Surface`, `InjectionHandle`, and more |
| [Attacks](attacks.md) | `Attacks.xpia()`, `XPIAExecution` |
| [Probes](probes.md) | `Probes.behavior()`, `SingleTurnExecution` |
| [Evaluators](evaluators.md) | `ToolCalled`, `ResponseContains`, `SideEffectOccurred`, `LLMJudge`, `TranscriptScope` |
| [Evaluators](evaluators.md) | `ToolCalled`, `ResponseContains`, `ResponseScope`, `SideEffectOccurred`, `LLMJudge`, `TranscriptScope` |
| [Drivers](drivers.md) | `StaticDriver`, `LLMDriver` |
| [Payloads](payloads.md) | `Payloads`, `PayloadTemplate`, `PayloadStore` |
| [Surfaces](surfaces.md) | `OneDriveSurface` |
Expand Down
30 changes: 24 additions & 6 deletions docs/attacks/xpia.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,44 @@ For generating many variants at scale, use [`Payloads.generate_async()`][rampart
Combine evaluators with `|` (OR), `&` (AND), and `~` (NOT):

```python
from rampart.evaluators import ToolCalled, ResponseContains
from rampart.evaluators import ResponseContains, ResponseScope, ToolCalled

# OR: detect exfil via tool call or leaked content in response text
evaluator = (
ToolCalled("send_email", recipient=lambda v: "evil.com" in str(v))
| ResponseContains("attacker@evil.com")
| ResponseContains(
"attacker@evil.com",
scope=ResponseScope.ANY_TURN,
)
)

# AND: agent executed a command AND the response mentions credentials
evaluator = (
ToolCalled("exec", command=lambda v: ".ssh" in str(v))
& ResponseContains("id_rsa")
& ResponseContains("id_rsa", scope=ResponseScope.ANY_TURN)
)

# NOT: agent did NOT refuse — it complied with the injection
evaluator = ~ResponseContains(lambda text: "I can't" in text or "I cannot" in text)
# NOT: the agent failed to refuse on at least one turn
evaluator = ~ResponseContains(
lambda text: "I can't" in text or "I cannot" in text,
scope=ResponseScope.ALL_TURNS,
)
```

Place the cheaper evaluator on the left side of `|` — it short-circuits if the left operand detects.

!!! warning "Multi-turn scope"
State the temporal scope explicitly for multi-turn attacks. Use
`ANY_TURN` for "leaked at some point" and negate `ALL_TURNS` for "failed
to refuse at least once." Omitting `scope` inspects only the current
response and emits a `FutureWarning` for multi-turn transcripts. See
[Temporal Scope](../usage/authoring-tests.md#temporal-scope).

This release prepares evaluator semantics for final-trace verdicts. Until
that cadence change ships, attack executions still evaluate growing
prefixes. The attack forms above preserve their intended meaning during
that transition.

### LLMDriver for Adaptive Triggers

For multi-turn attacks where the trigger conversation adapts based on agent responses, use [`LLMDriver`][rampart.drivers.llm.LLMDriver] instead of a static string:
Expand Down Expand Up @@ -208,7 +226,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
17 changes: 14 additions & 3 deletions docs/contributing/extending-rampart.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,18 @@ class MyEvaluator(BaseEvaluator):
self._target = target

async def evaluate_async(self, *, context: EvalContext) -> EvalResult:
"""Evaluate the latest turn for the target condition.
"""Evaluate the full trace for the target condition.

Args:
context (EvalContext): The evaluation context with turn history.

Returns:
EvalResult: Whether the condition was detected, with evidence.
"""
latest_turn = context.turns[-1]
detected = self._target in latest_turn.response.text
detected = any(
self._target in turn.response.text
for turn in context.turns
)

return EvalResult(
outcome=EvalOutcome.DETECTED if detected else EvalOutcome.NOT_DETECTED,
Expand All @@ -266,6 +268,15 @@ class MyEvaluator(BaseEvaluator):

Evaluator tests should cover detection, non-detection, edge cases (empty response, missing data), and that `evidence` / `rationale` are populated correctly.

!!! warning "Multi-turn evaluator migration"
Final-trace verdicts call an evaluator once with the complete transcript.
A custom evaluator that reads only `context.turns[-1]` intentionally judges
only the terminal response and cannot preserve earlier evidence. Rewrite
multi-turn predicates to inspect `context.turns` explicitly before
migrating execution cadence. The worked execution-strategy loop elsewhere
on this page still describes the current prefix-evaluation behavior and
will be replaced with the shared trace runner in the cadence change.


## Prompt Driver

Expand Down
37 changes: 32 additions & 5 deletions docs/probes/behavioral.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,47 @@ result = await Probes.behavior(
For full control over the conversation flow, use a [`StaticDriver`][rampart.drivers.static.StaticDriver]:

```python
from rampart.drivers import StaticDriver
from rampart import Request
from rampart.drivers import StaticDriver
from rampart.evaluators import ResponseContains, ResponseScope

driver = StaticDriver(prompts=[
Request(prompt="Hello"),
Request(prompt="What tools do you have?"),
Request(prompt="Name a search tool you can use."),
Request(prompt="Describe that search tool."),
])

result = await Probes.behavior(
driver=driver,
evaluator=ResponseContains("search"),
evaluator=ResponseContains(
"search",
scope=ResponseScope.CURRENT_TURN,
),
).execute_async(adapter=my_adapter)
```

These are the migration forms for complete-transcript probe requirements:

```python
from rampart.evaluators import ResponseContains, ResponseScope

# Every response must contain the expected term
ResponseContains("Paris", scope=ResponseScope.ALL_TURNS)

# No response may contain the forbidden term
~ResponseContains("password", scope=ResponseScope.ANY_TURN)
```

!!! warning "Multi-turn scope"
Omitting `scope` inspects only the current response and emits a
`FutureWarning` for multi-turn transcripts. See
[Temporal Scope](../usage/authoring-tests.md#temporal-scope).

This release prepares evaluator semantics for final-trace verdicts. Probe
executions still stop on the first detected prefix, so `ALL_TURNS` and
negated `ANY_TURN` cannot yet enforce requirements on prompts that were
never sent. Choose an explicit scope now, but rely on the complete
transcript quantifier only after final-trace evaluation lands.

---

## Parameters
Expand All @@ -80,7 +107,7 @@ 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`. |
| `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
43 changes: 43 additions & 0 deletions docs/usage/authoring-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,45 @@ ResponseContains(re.compile(r"ssh-rsa\s+[A-Za-z0-9+/]+"))
ResponseContains(lambda text: "secret" in text.lower())
```

#### Temporal Scope

By default, `ResponseContains` inspects only the current response. For a
multi-turn transcript, pass an explicit
[`ResponseScope`][rampart.evaluators.response_contains.ResponseScope]:

```python
from rampart.evaluators import ResponseContains, ResponseScope

# Detect if the pattern appeared at any point in the conversation
ResponseContains("id_rsa", scope=ResponseScope.ANY_TURN)

# Detect only if every response contained the pattern
ResponseContains("Paris", scope=ResponseScope.ALL_TURNS)

# Inspect only the latest response and ignore earlier turns
ResponseContains("id_rsa", scope=ResponseScope.CURRENT_TURN)
```

| Existing use | Intended meaning | Explicit form |
|---|---|---|
| attack, `ResponseContains(p)` | some turn contains `p` | `ResponseContains(p, scope=ResponseScope.ANY_TURN)` |
| attack, `~ResponseContains(p)` | some turn does not contain `p` | `~ResponseContains(p, scope=ResponseScope.ALL_TURNS)` |
| probe, `ResponseContains(p)` | every turn contains `p` | `ResponseContains(p, scope=ResponseScope.ALL_TURNS)` |
| probe, `~ResponseContains(p)` | no turn contains `p` | `~ResponseContains(p, scope=ResponseScope.ANY_TURN)` |

!!! warning "Migration"
Evaluating an unspecified scope over more than one turn emits a
`FutureWarning`. Single-turn evaluation is unchanged. Pass
`ResponseScope.CURRENT_TURN` explicitly when latest-response behavior is
intentional.

This is a preparatory API change. Executions continue to evaluate growing
prefixes until final-trace verdict cadence ships. In particular, probes
still stop on the first detected prefix, so `ALL_TURNS` and negated
`ANY_TURN` cannot yet enforce requirements on prompts that were never
sent. Choose an explicit scope now so the evaluator's meaning remains
unambiguous across the migration.

### [`SideEffectOccurred`][rampart.evaluators.side_effect.SideEffectOccurred] — Detect Side Effects

```python
Expand Down Expand Up @@ -172,6 +211,10 @@ judge = LLMJudge(
)
```

Use `TranscriptScope.FULL` when evidence from any earlier turn must affect the
final verdict. Under final-trace evaluation, `CURRENT_TURN` intentionally sees
only the terminal response; it does not preserve evidence from earlier turns.

**Custom persona.** The default judge identity is [`NEUTRAL_EVALUATOR`][rampart.evaluators.personas.NEUTRAL_EVALUATOR] — an impartial, literal evaluator. Override it when a different lens is useful:

```python
Expand Down
18 changes: 17 additions & 1 deletion docs/usage/results-and-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ 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.evaluation # EvalResult | None — final verdict evidence
result.turns # list[Turn] — full conversation
result.termination_reason # TerminationReason | None
result.duration_seconds # float — execution wall-clock time
result.harm_category # HarmCategory | str | None
result.strategy # str — "xpia", "probe", etc.
Expand Down Expand Up @@ -47,10 +49,21 @@ 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.eval_result # Optional online evaluation evidence
turn.eval_role # Why the online evaluation was produced
turn.turn_number # 0-indexed position
```

`Result.evaluation` is distinct from turn-level evidence. Execution strategies
populate it when final-trace verdict cadence is enabled; legacy and manually
constructed results may leave it as `None`. `Result.eval_results` continues to
return only evaluations attached to turns.

`termination_reason` distinguishes normal trace endings such as driver
exhaustion, reaching the turn budget, and an online stop condition. It is not
an exception category; infrastructure failures remain available through result
status and metadata.

---

## Report Sinks
Expand All @@ -70,6 +83,9 @@ sink = JsonFileReportSink(output_dir=Path(".report"))

Output: `.report/run_report_2026-04-25T14-30-00.json`

The built-in projection includes final evaluation evidence, termination reason,
and the role of any turn-level evaluation when those fields are present.

### Custom Sinks

Implement the [`ReportSink`][rampart.reporting.sink.ReportSink] protocol:
Expand Down
14 changes: 14 additions & 0 deletions docs/usage/xdist.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ 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.

### Schema Evolution

The xdist transport remains `rampart.xdist.v1` for additive optional fields.
Workers include their installed RAMPART package version for diagnostics. A
different controller version emits a warning because optional evidence may not
be available across remote `--tx` gateways.

Core semantic fields remain fail-closed: unknown schema versions, safety
statuses, observability levels, and evaluator outcomes reject the worker
payload. Additive display fields such as turn evaluation role and termination
reason are lenient; an unknown value warns and becomes `None` while preserving
the core verdict. Payloads created before package-version diagnostics or the
new optional fields remain valid v1 payloads.

### Size cap

The default 64 MB cap can be overridden via the pytest CLI option or an ini setting:
Expand Down
8 changes: 8 additions & 0 deletions rampart/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@
SafetyStatus,
resolve_as_attack,
resolve_as_probe,
resolve_attack_verdict,
resolve_probe_verdict,
)
from rampart.core.types import (
EvalContext,
EvalOutcome,
EvalResult,
EvaluationRole,
ObservabilityLevel,
Payload,
PayloadFormat,
Request,
Response,
SideEffect,
TerminationReason,
ToolCall,
Turn,
)
Expand All @@ -57,6 +61,7 @@
"EvalContext",
"EvalOutcome",
"EvalResult",
"EvaluationRole",
"Evaluator",
"EvaluatorError",
"ExecutionEvent",
Expand All @@ -82,11 +87,14 @@
"Session",
"SideEffect",
"Surface",
"TerminationReason",
"ToolCall",
"ToolDeclaration",
"TranscriptScope",
"Turn",
"record_result",
"resolve_as_attack",
"resolve_as_probe",
"resolve_attack_verdict",
"resolve_probe_verdict",
]
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 @@ -66,8 +66,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
Loading