Skip to content
Closed
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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- name: Pyright
run: uv run pyright

- name: RAMPART lint
run: uv run python -m tools.rampart_lint check rampart/

test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
Expand Down Expand Up @@ -77,4 +80,5 @@ jobs:

- name: Run tests
# Coverage threshold (fail_under) is enforced via pyproject.toml [tool.coverage.report]
run: uv run pytest tests/unit --cov=rampart --cov-report=xml --cov-report=term-missing
# Test paths configured via testpaths in pyproject.toml
run: uv run pytest --cov=rampart --cov-report=xml --cov-report=term-missing
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ repos:
language: system
types: [python]
pass_filenames: false
- id: rampart-lint
name: rampart-lint (RAMPART001, RAMPART002)
entry: uv run python -m tools.rampart_lint check
language: system
types: [python]
files: ^rampart/
pass_filenames: true
24 changes: 23 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,22 @@ skip_empty = true
[tool.pyright]
pythonVersion = "3.11"
typeCheckingMode = "strict"
include = ["rampart", "tests"]
include = ["rampart", "tests", "tools"]

[[tool.pyright.executionEnvironments]]
root = "tests"
extraPaths = ["."]
reportPrivateUsage = false

[[tool.pyright.executionEnvironments]]
root = "tools"
extraPaths = ["."]
reportPrivateUsage = false

[tool.pytest.ini_options]
asyncio_mode = "auto"
pythonpath = ["."]
testpaths = ["tests/unit", "tools/rampart_lint/tests"]
markers = [
"harm(*categories): categorize test by harm type",
"trial(n=, threshold=): statistical repetition of a test",
Expand All @@ -87,6 +94,7 @@ filterwarnings = [

[tool.ruff.lint]
select = ["ALL"]
external = ["RAMPART001", "RAMPART002"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = [
Expand All @@ -105,6 +113,20 @@ select = ["ALL"]
"ASYNC240", # pathlib in async tests
"PERF401", "RUF015", "PTH123", # micro-optimizations / style
]
"tools/**/tests/**" = [
"INP001", # test dirs don't need __init__.py
"S101", # assert is pytest's API
"D100", "D101", "D102", "D104", "D107", # no docstrings needed
"ANN001", "ANN201", "ANN202", "ANN401", # no type annotations needed
"PLR2004", # magic values in assertions are fine
"ARG001", "ARG002", # unused args (fixtures, stubs)
"PLC0415", # imports inside functions for isolation
"SLF001", # testing private members is valid
"TRY003", "EM101", "EM102", # exception message style
]
"tools/**" = [
"T201", # print is the CLI's output mechanism
]

[tool.ruff.lint.flake8-copyright]
notice-rgx = "Copyright \\(c\\) Microsoft Corporation\\.\\s*\\n.*Licensed under the MIT license"
Expand Down
2 changes: 1 addition & 1 deletion rampart/attacks/_xpia.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async def _activate_handles_async(
# Concurrent: total = max of all wait times
async with asyncio.TaskGroup() as tg:
for handle in self._handles:
tg.create_task(handle.wait_until_ready())
tg.create_task(handle.wait_until_ready_async())

def _build_attack_result(
self,
Expand Down
12 changes: 6 additions & 6 deletions rampart/core/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ExecutionEventHandler(ABC):
"""

@abstractmethod
async def on_event(self, *, event_data: ExecutionEventData) -> None:
async def on_event_async(self, *, event_data: ExecutionEventData) -> None:
"""Handle an execution lifecycle event.

Args:
Expand Down Expand Up @@ -228,7 +228,7 @@ async def execute_async(self, *, adapter: AgentAdapter) -> Result:
_execute_async, after notifying handlers via ON_ERROR.
"""
start = time.monotonic()
await self._fire(
await self._fire_async(
ExecutionEvent.ON_PRE_EXECUTE,
adapter=adapter,
elapsed=0.0,
Expand All @@ -253,7 +253,7 @@ async def execute_async(self, *, adapter: AgentAdapter) -> Result:
)
except Exception as exc:
elapsed = time.monotonic() - start
await self._fire(
await self._fire_async(
ExecutionEvent.ON_ERROR,
adapter=adapter,
elapsed=elapsed,
Expand All @@ -263,7 +263,7 @@ async def execute_async(self, *, adapter: AgentAdapter) -> Result:

elapsed = time.monotonic() - start
result.duration_seconds = elapsed
await self._fire(
await self._fire_async(
ExecutionEvent.ON_POST_EXECUTE,
adapter=adapter,
elapsed=elapsed,
Expand All @@ -283,7 +283,7 @@ async def _execute_async(self, *, adapter: AgentAdapter) -> Result:
"""
...

async def _fire(
async def _fire_async(
self,
event: ExecutionEvent,
*,
Expand Down Expand Up @@ -313,7 +313,7 @@ async def _fire(
)
for handler in self._handlers:
try:
await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)
except Exception: # noqa: BLE001 — handler errors must not break execution
logger.warning(
"ExecutionEventHandler %s raised on %s — ignored.",
Expand Down
6 changes: 3 additions & 3 deletions rampart/core/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Two protocols serving two audiences: Surface is what surface authors
implement; InjectionHandle is what execution strategies consume.

``sleep_until_ready`` is a helper function for surfaces that only need
``sleep_until_ready_async`` is a helper function for surfaces that only need
a simple delay-based readiness wait.
"""

Expand Down Expand Up @@ -43,7 +43,7 @@ def surface_name(self) -> str:
"""The name of the surface this handle injects into (e.g., 'SharePoint')."""
...

async def wait_until_ready(self) -> None:
async def wait_until_ready_async(self) -> None:
"""Block until the injected content is visible to the agent.

Implementations should raise `TimeoutError` if readiness
Expand All @@ -65,7 +65,7 @@ async def __aexit__(
...


async def sleep_until_ready(delay: float) -> None:
async def sleep_until_ready_async(delay: float) -> None:
"""Sleep for `delay` seconds. Default readiness strategy for simple surfaces.

Args:
Expand Down
2 changes: 1 addition & 1 deletion rampart/pytest_plugin/_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class ResultCollectionHandler(ExecutionEventHandler):
collector is active (safe to use outside pytest).
"""

async def on_event(self, *, event_data: ExecutionEventData) -> None:
async def on_event_async(self, *, event_data: ExecutionEventData) -> None:
"""Record result on post-execute. Ignore all other events.

Args:
Expand Down
12 changes: 6 additions & 6 deletions rampart/pytest_plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ def _create_trial_clones(


@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(
config: pytest.Config, # noqa: ARG001 — pytest hook signature
def pytest_collection_modifyitems( # noqa: RAMPART002 — pytest hook signature
config: pytest.Config, # noqa: ARG001
items: list[pytest.Item],
) -> None:
"""Clone trial-marked items and validate marker usage.
Expand Down Expand Up @@ -469,9 +469,9 @@ def _evaluate_gates(
)


def pytest_sessionfinish(
def pytest_sessionfinish( # noqa: RAMPART002 — pytest hook signature
session: pytest.Session,
exitstatus: int, # noqa: ARG001 — pytest hook signature
exitstatus: int, # noqa: ARG001
) -> None:
"""Aggregate trial results, evaluate gates, and emit sinks.

Expand Down Expand Up @@ -598,9 +598,9 @@ def _write_trial_group_lines(
)


def pytest_terminal_summary(
def pytest_terminal_summary( # noqa: RAMPART002 — pytest hook signature
terminalreporter: TerminalReporter,
exitstatus: int, # noqa: ARG001 — pytest hook signature
exitstatus: int, # noqa: ARG001
config: pytest.Config,
) -> None:
"""Append RAMPART harm-category summary after pytest's standard output.
Expand Down
6 changes: 3 additions & 3 deletions rampart/surfaces/onedrive.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import TYPE_CHECKING, Self

from rampart.core.errors import InfrastructureError
from rampart.core.injection import sleep_until_ready
from rampart.core.injection import sleep_until_ready_async

if TYPE_CHECKING:
import types
Expand Down Expand Up @@ -196,14 +196,14 @@ def surface_name(self) -> str:
"""Identifies this injection as OneDrive for reporting."""
return "OneDrive"

async def wait_until_ready(self) -> None:
async def wait_until_ready_async(self) -> None:
"""Wait for the uploaded content to be indexed and discoverable.

Note: Currently sleeps for `OneDriveSurface.indexing_delay` seconds.
Future versions will poll the Graph API for content availability instead and
raise `TimeoutError` if it doesn't appear within the `indexing_delay`.
"""
await sleep_until_ready(delay=self._surface.indexing_delay)
await sleep_until_ready_async(delay=self._surface.indexing_delay)

async def __aenter__(self) -> Self:
"""Upload payload to OneDrive. Raises InfrastructureError on failure."""
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/attacks/test_xpia.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ async def test_handle_entered_and_exited(self) -> None:

handle.__aenter__.assert_awaited_once()
handle.__aexit__.assert_awaited_once()
handle.wait_until_ready.assert_awaited_once()
handle.wait_until_ready_async.assert_awaited_once()

@pytest.mark.asyncio
async def test_multiple_handles_all_cleaned(self) -> None:
Expand All @@ -193,7 +193,7 @@ async def test_multiple_handles_all_cleaned(self) -> None:
for h in (h1, h2):
h.__aenter__.assert_awaited_once()
h.__aexit__.assert_awaited_once()
h.wait_until_ready.assert_awaited_once()
h.wait_until_ready_async.assert_awaited_once()

@pytest.mark.asyncio
async def test_cleanup_on_evaluator_exception(self) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/core/test_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ class _RecordingHandler(ExecutionEventHandler):
def __init__(self) -> None:
self.events: list[ExecutionEventData] = []

async def on_event(self, *, event_data: ExecutionEventData) -> None:
async def on_event_async(self, *, event_data: ExecutionEventData) -> None:
"""Record the event data."""
self.events.append(event_data)


class _BrokenHandler(ExecutionEventHandler):
"""Handler that always raises."""

async def on_event(self, *, event_data: ExecutionEventData) -> None:
async def on_event_async(self, *, event_data: ExecutionEventData) -> None:
"""Raise unconditionally to test handler safety."""
raise ValueError("handler broke")

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/core/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def payload_id(self) -> str | None:
def surface_name(self) -> str:
return "SharePoint"

async def wait_until_ready(self) -> None:
async def wait_until_ready_async(self) -> None:
pass

async def __aenter__(self) -> Self:
Expand Down Expand Up @@ -114,7 +114,7 @@ def payload_id(self) -> str | None:
def surface_name(self) -> str:
return "test"

async def wait_until_ready(self) -> None:
async def wait_until_ready_async(self) -> None:
pass

async def __aenter__(self) -> Self:
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/pytest_plugin/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def test_records_on_post_execute_async(self) -> None:
result=result,
)

await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)

assert len(collector.results) == 1
assert collector.results[0].summary == "captured"
Expand All @@ -98,7 +98,7 @@ async def test_ignores_pre_execute_async(self) -> None:
handler = ResultCollectionHandler()
event_data = _make_event_data(event=ExecutionEvent.ON_PRE_EXECUTE)

await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)

assert collector.results == []
finally:
Expand All @@ -112,7 +112,7 @@ async def test_ignores_on_error_async(self) -> None:
handler = ResultCollectionHandler()
event_data = _make_event_data(event=ExecutionEvent.ON_ERROR)

await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)

assert collector.results == []
finally:
Expand All @@ -127,7 +127,7 @@ async def test_noop_when_no_collector_active_async(self) -> None:
result=result,
)

await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)

@pytest.mark.asyncio
async def test_noop_when_result_is_none_async(self) -> None:
Expand All @@ -140,7 +140,7 @@ async def test_noop_when_result_is_none_async(self) -> None:
result=None,
)

await handler.on_event(event_data=event_data)
await handler.on_event_async(event_data=event_data)

assert collector.results == []
finally:
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/surfaces/test_onedrive.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,11 @@ async def test_infrastructure_error_from_upload_not_double_wrapped(self) -> None


class TestOneDriveInjectionWaitUntilReady:
"""Test _OneDriveInjection.wait_until_ready wiring."""
"""Test _OneDriveInjection.wait_until_ready_async wiring."""

@pytest.mark.asyncio
async def test_delegates_to_sleep_until_ready(self) -> None:
"""Verifies correct arguments are passed to sleep_until_ready."""
async def test_delegates_to_sleep_until_ready_async(self) -> None:
"""Verifies correct arguments are passed to sleep_until_ready_async."""
surface = OneDriveSurface(
graph_client=MagicMock(),
drive_id="d",
Expand All @@ -367,9 +367,9 @@ async def test_delegates_to_sleep_until_ready(self) -> None:
handle = surface.inject(payload=Payload(content="test"))

with patch(
"rampart.surfaces.onedrive.sleep_until_ready",
"rampart.surfaces.onedrive.sleep_until_ready_async",
new_callable=AsyncMock,
) as mock_sleep:
await handle.wait_until_ready()
await handle.wait_until_ready_async()

mock_sleep.assert_awaited_once_with(delay=5.0)
4 changes: 4 additions & 0 deletions tools/rampart_lint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""RAMPART custom lint rules package."""
Loading
Loading