Skip to content
Merged
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 .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Thank you for your interest in contributing to RAMPART! For comprehensive contributor documentation — including development setup, code style, testing standards, PR process, and how to add new attacks/probes — see the **[Contributing Guide](https://microsoft.github.io/RAMPART/contributing/)**.

## Contributor License Agreement

Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com).

When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.

## Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
91 changes: 91 additions & 0 deletions docs/contributing/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Architecture for Contributors

This page supplements the [Concepts Overview](../concepts/overview.md) with contributor-focused guidance: package layout, extension points, and key design decisions to understand before making changes. Read the **Concepts Overview** first for the component model and execution lifecycle.

---

## Package Layout

The `rampart/` source tree is organized by concern: foundational types live in `core/`, while each extension point gets its own subpackage (`attacks/`, `probes/`, `evaluators/`, `drivers/`, `converters/`, `surfaces/`, `payloads/`, `reporting/`). The `pyrit_bridge/` package groups common PyRIT integration code (see [PyRIT Bridge](#pyrit-bridge) below), and `pytest_plugin/` provides the pytest integration.

- For the component model and how the pieces fit together at runtime, see the [Concepts Overview](../concepts/overview.md).
- For the public symbols exported from each package, see the [API Reference index](../api/index.md).
- For where to put new code, see [Extension Points](#extension-points) below.
- The authoritative layout is always the source tree itself — browse [`rampart/` on GitHub](https://github.com/microsoft/RAMPART/tree/main/rampart).

---

## Key Design Decisions

### Protocols Over ABCs

RAMPART uses `@runtime_checkable` protocols for extension points that consumers implement (`AgentAdapter`, `Session`, `Evaluator`, `Surface`, `PromptDriver`). This means:

- **No inheritance required** — any class with the right methods satisfies the protocol
- **Type-checked at development time** by Pyright in strict mode
- **Verifiable at runtime** with `isinstance` checks

`BaseExecution` is the exception — it's an ABC because it owns the lifecycle skeleton and subclasses share real implementation.

### Factory Classes (`Attacks`, `Probes`)

`Attacks` and `Probes` are **static factory classes** that construct execution objects. They:

- Provide a clean, discoverable API: `Attacks.xpia(...)`, `Probes.behavior(...)`
- Handle input coercion (e.g., `coerce_driver` for flexible trigger input)
- Return `BaseExecution`, hiding the concrete execution class

When adding a new attack or probe, you add a static factory method — not a new class that users instantiate directly.

### Evaluator Polarity

Evaluators are **polarity-free**. They report whether a condition was detected, not whether it's good or bad. The attack/probe factory applies the correct polarity:

- `resolve_as_attack`: detected → UNSAFE
- `resolve_as_probe`: detected → SAFE

This allows the same evaluator (e.g., `ToolCalled`) to be used in both attack and probe contexts.

### Execution Lifecycle Ownership

`BaseExecution` owns all cross-cutting concerns:

- **Event dispatch** — ON_PRE_EXECUTE, ON_POST_EXECUTE, ON_ERROR
- **Timing** — wall-clock duration
- **Error handling** — all exceptions from `_execute_async` are caught and converted to ERROR results
- **Handler registration** — framework-level handlers (result collection) are injected automatically

Subclasses implement only `_execute_async` and `strategy_name`. They should **not** catch `InfrastructureError` — the base class handles it.

### PyRIT Bridge

PyRIT is RAMPART's upstream dependency for converters and prompt generation. Its import chain is heavy, so:

- PyRIT-related logic is grouped under `rampart/pyrit_bridge/`
- Lazy imports inside functions are used to defer the cost
- This boundary keeps RAMPART's core import fast

---

## Extension Points

| Extension Point | Protocol/ABC | Where to add |
|----------------|-------------|--------------|
| New attack strategy | Subclass `BaseExecution` | `rampart/attacks/` |
| New probe strategy | Subclass `BaseExecution` | `rampart/probes/` |
| New evaluator | Implement `Evaluator` protocol | `rampart/evaluators/` |
| New prompt driver | Implement `PromptDriver` protocol | `rampart/drivers/` |
| New attack surface | Implement `Surface` protocol | `rampart/surfaces/` |
| New converter | Implement `PayloadConverter` protocol | `rampart/converters/` |
| New report sink | Implement `ReportSink` protocol | `rampart/reporting/` |
| New payload format | Extend payload system | `rampart/payloads/` |

See [Extending RAMPART](extending-rampart.md) for step-by-step guides.

---

## Module Import Conventions

- Import from the package root (`rampart.core`, `rampart.attacks`) when the symbol is exported in `__init__.py`
- Within the same package, import from the specific module to avoid circular imports
- Internal modules are prefixed with `_` (e.g., `_xpia.py`, `_single_turn.py`) — they are not part of the public API
172 changes: 172 additions & 0 deletions docs/contributing/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Code Style & Linting

RAMPART enforces a consistent code style through automated tooling and documented conventions. This page summarizes the key rules; for the complete reference, see the [coding standards](https://github.com/microsoft/RAMPART/blob/main/.github/instructions/coding-standards.instructions.md).

## Toolchain

| Tool | Purpose | Config location |
|------|---------|-----------------|
| [Ruff](https://docs.astral.sh/ruff/) | Linting and formatting | `pyproject.toml` `[tool.ruff.*]` |
| [Pyright](https://github.com/microsoft/pyright) | Static type checking (strict mode) | `pyproject.toml` `[tool.pyright]` |
| [pre-commit](https://pre-commit.com/) | Git hooks for automated checks | `.pre-commit-config.yaml` |

### Running Checks

Pre-commit is the primary entry point — it runs Ruff (lint + format) and Pyright in one command:

```bash
# Install the Git hook once (optional, runs on every commit)
uv run pre-commit install

# Run all checks on demand
uv run pre-commit run --all-files
```

When checks fail, Ruff can auto-fix most lint and formatting issues:

```bash
uv run ruff check --fix .
uv run ruff format .
```

A few details worth knowing:

- **Ruff** is configured with `select = ["ALL"]`. Test files have relaxed rules (no docstrings, no type annotations, magic values allowed) via `per-file-ignores` in `pyproject.toml`.
- **Pyright** runs in **strict mode** targeting Python 3.11 — every function needs complete parameter and return type annotations.


## Key Conventions

### Copyright Header

Every `.py` file **must** begin with:

```python
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
```

This is enforced by Ruff's copyright rule.

### Async Function Naming

All async functions **must** end with `_async`:

```python
# Correct
async def send_request_async(self, *, payload: str) -> Response: ...

# Incorrect
async def send_request(self, *, payload: str) -> Response: ...
```

Dunder methods (`__aenter__`, `__aexit__`) are exempt.

### Keyword-Only Arguments

Functions with more than one parameter **must** use `*` to enforce keyword-only arguments:

```python
# Correct
def __init__(self, *, client: ServiceClient, config: Config) -> None: ...

# Incorrect
def __init__(self, client: ServiceClient, config: Config) -> None: ...
```

Dunder methods with Python-defined signatures (`__or__`, `__eq__`, etc.) are exempt.

### Type Annotations

Every function parameter and return type **must** have explicit type annotations:

```python
# Correct
def process(self, *, items: list[str], limit: int = 10) -> dict[str, Any]: ...

# Incorrect
def process(self, items, limit=10): ...
```

Use modern syntax: `str | None` instead of `Optional[str]`, `list[str]` instead of `List[str]`.

### Enums Over Literals

Use `Enum` or `StrEnum` instead of `Literal` types for predefined choices:

```python
# Correct
class Status(Enum):
PENDING = "pending"
COMPLETE = "complete"

# Incorrect
def classify(self, *, status: Literal["pending", "complete"]) -> None: ...
```

### Import Organization

Imports are organized in three groups separated by blank lines:

1. Standard library
2. Third-party packages
3. Local application imports

Import from the package root (`__init__.py`) when the symbol is exported there, not from internal file paths.

All imports must live at the top of the file — inline/local imports inside functions are forbidden, except to break circular dependencies or to defer heavy import chains (see [PyRIT Bridge](#pyrit-bridge) below).

### Logging

Use `%s`-style lazy formatting in log calls — **not** f-strings:

```python
# Correct
logger.info("Saved %d payloads to '%s'", len(payloads), name)

# Incorrect
logger.info(f"Saved {len(payloads)} payloads to '{name}'")
```

### Docstrings

Use Google-style docstrings with `Args:`, `Returns:`, and `Raises:` sections. Do not include example usage in docstrings.

### PyRIT Bridge

Common PyRIT integration logic should be grouped under `rampart/pyrit_bridge/`.
Two rules apply:

1. Do not expose PyRIT-specific types in RAMPART's public APIs. Translate to/from RAMPART types at the bridge boundary so consumers don't have to depend on PyRIT directly.
2. Defer heavy PyRIT imports with lazy imports inside functions. PyRIT's import chain is heavy, so importing it at module top-level slows down RAMPART's startup. Use a local import where the PyRIT type is actually needed:

```python
def _get_converter(self) -> WordDocConverter:
"""PyRIT's import chain is heavy (~14s), so defer until first use."""
from pyrit.prompt_converter.word_doc_converter import WordDocConverter # noqa: PLC0415

return WordDocConverter()
```


## Quick Reference Checklist

Before committing, run pre-commit — it covers everything the automated tooling can verify:

```bash
uv run pre-commit run --all-files
```

This runs Ruff (linting + formatting) and Pyright (strict type checking), which together enforce the copyright header, type annotations, log formatting, import organization, and most other conventions on this page.

A few rules are **not** caught by tooling and still need a human eye:

- [ ] All async functions end with `_async`
- [ ] Functions with more than one parameter use keyword-only arguments (`*`)
- [ ] `Enum` / `StrEnum` is used instead of `Literal` for predefined choices
- [ ] PyRIT imports are lazy (inside functions), not module-level

!!! tip
**Use GitHub Copilot to cross-check.** GitHub Copilot in VS Code automatically picks up the repo's [coding standards](https://github.com/microsoft/RAMPART/blob/main/.github/instructions/coding-standards.instructions.md) (via `.github/instructions/`) and can review your changes against them. Ask Copilot Chat something like *"Review my staged changes against the RAMPART coding standards"* to get a second pass on the conventions above before you commit.

If `pre-commit run --all-files` passes and the items above hold, you're ready to commit.
Loading
Loading