Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/probes/behavioral.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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
24 changes: 24 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,31 @@ 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.

This layer makes terminal provenance durable before changing execution
cadence. Existing prefix-evaluated strategies leave these fields as `None`
until their follow-up migration; 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
11 changes: 11 additions & 0 deletions docs/usage/xdist.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,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
8 changes: 8 additions & 0 deletions rampart/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,22 @@
SafetyStatus,
resolve_as_attack,
resolve_as_probe,
resolve_attack_verdict,
resolve_probe_verdict,
)
from rampart.core.types import (
EvalContext,
EvalOutcome,
EvalResult,
EvaluationPurpose,
ObservabilityLevel,
Payload,
PayloadFormat,
Request,
Response,
SideEffect,
ToolCall,
TraceEndReason,
Turn,
)

Expand All @@ -58,6 +62,7 @@
"EvalContext",
"EvalOutcome",
"EvalResult",
"EvaluationPurpose",
"Evaluator",
"ExecutionEvent",
"ExecutionEventData",
Expand Down Expand Up @@ -86,9 +91,12 @@
"Surface",
"ToolCall",
"ToolDeclaration",
"TraceEndReason",
"Turn",
"evaluate_turn_async",
"execute_trials_async",
"resolve_as_attack",
"resolve_as_probe",
"resolve_attack_verdict",
"resolve_probe_verdict",
]
124 changes: 124 additions & 0 deletions rampart/core/_population.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Shared validation for trial population configuration and provenance."""

from __future__ import annotations

import math


def validate_population_id(value: object) -> str:
"""Validate and return a population identifier.

Returns:
str: Validated population identifier.

Raises:
TypeError: If ``value`` is not a string.
ValueError: If ``value`` is empty or exceeds the transport bound.
"""
if not isinstance(value, str):
msg = "population id must be a string"
raise TypeError(msg)
if not value:
msg = "population id must be non-empty"
raise ValueError(msg)
return value


def validate_population_size(
value: object,
*,
name: str,
allow_zero: bool = False,
) -> int:
"""Validate and return a population size.

Returns:
int: Validated population size.

Raises:
TypeError: If ``value`` is not a non-boolean integer.
ValueError: If ``value`` is outside the supported range.
"""
if type(value) is not int:
msg = f"{name} must be a non-boolean integer"
raise TypeError(msg)
minimum = 0 if allow_zero else 1
if value < minimum:
if minimum == 0:
msg = f"{name} must be greater than or equal to 0"
else:
msg = f"{name} must be greater than or equal to 1"
raise ValueError(msg)
return value


def validate_population_threshold(value: object, *, name: str) -> float:
"""Validate and return a finite population threshold.

Returns:
float: Normalized population threshold.

Raises:
TypeError: If ``value`` is not a non-boolean number.
ValueError: If ``value`` is non-finite or outside [0.0, 1.0].
"""
if isinstance(value, bool) or not isinstance(value, int | float):
msg = f"{name} must be a number"
raise TypeError(msg)
try:
normalized = float(value)
except OverflowError as exc:
msg = f"{name} must be finite"
raise ValueError(msg) from exc
if not math.isfinite(normalized):
msg = f"{name} must be finite"
raise ValueError(msg)
if not 0.0 <= normalized <= 1.0:
msg = f"{name} must be between 0.0 and 1.0"
raise ValueError(msg)
return normalized


def validate_population_index(value: object, *, size: int) -> int:
"""Validate and return a population member index.

Returns:
int: Validated population index.

Raises:
TypeError: If ``value`` is not a non-boolean integer.
ValueError: If ``value`` falls outside the population.
"""
if type(value) is not int:
msg = "population index must be an integer"
raise TypeError(msg)
if not 0 <= value < size:
msg = "population index must be between 0 and size - 1"
raise ValueError(msg)
return value


def validate_population_parameters(
*,
size: object,
threshold: object,
size_name: str,
threshold_name: str,
allow_empty: bool = False,
) -> tuple[int, float]:
"""Validate and normalize shared population parameters.

Returns:
tuple[int, float]: Validated size and normalized threshold.
"""
return (
validate_population_size(
size,
name=size_name,
allow_zero=allow_empty,
),
validate_population_threshold(threshold, name=threshold_name),
)
23 changes: 12 additions & 11 deletions rampart/core/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from rampart.core._population import validate_population_parameters
from rampart.core.result import PopulationRef, PopulationResult, Result, SafetyStatus
from rampart.core.types import (
EvalContext,
Expand Down Expand Up @@ -377,7 +378,7 @@ async def execute_trials_async(
TypeError: If n is not a non-boolean integer.
ValueError: If n is less than 1 or threshold is outside [0.0, 1.0].
"""
_validate_trial_parameters(
n, threshold = _validate_trial_parameters(
n=n,
threshold=threshold,
)
Expand Down Expand Up @@ -407,22 +408,22 @@ def _validate_trial_parameters(
*,
n: int,
threshold: float,
) -> None:
) -> tuple[int, float]:
"""Validate trial population parameters.

Returns:
tuple[int, float]: Validated count and normalized threshold.

Raises:
TypeError: If n is not a non-boolean integer.
ValueError: If n is less than 1 or threshold is outside [0.0, 1.0].
"""
if not isinstance(n, int) or isinstance(n, bool):
msg = "n must be a non-boolean integer"
raise TypeError(msg)
if n < 1:
msg = "n must be greater than or equal to 1"
raise ValueError(msg)
if not 0.0 <= threshold <= 1.0:
msg = "threshold must be between 0.0 and 1.0"
raise ValueError(msg)
return validate_population_parameters(
size=n,
threshold=threshold,
size_name="n",
threshold_name="threshold",
)


async def evaluate_turn_async(
Expand Down
Loading