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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class DefenseResult:

### `defense.defend_tool_results(items)`

Sync batch API. When `enable_tier3=True`, uses one `asyncio.run()` and defends items **concurrently** via `asyncio.gather` (same scheduling model as npm `defendToolResults`; blocking sync providers still run one at a time on the event-loop thread). From async code, prefer `defend_tool_results_async`.

```python
results = defense.defend_tool_results([
{"value": email_data, "tool_name": "gmail_get_message"},
Expand All @@ -189,6 +191,17 @@ for r in results:
print("Blocked:", ", ".join(r.fields_sanitized))
```

### `await defense.defend_tool_results_async(items)`

Async batch API — runs `defend_tool_result_async` per item concurrently via `asyncio.gather`. Required when Tier 3 is enabled inside a running event loop (e.g. FastAPI).

```python
results = await defense.defend_tool_results_async([
{"value": email_data, "tool_name": "gmail_get_message"},
{"value": doc_data, "tool_name": "documents_get"},
])
```

### `defense.analyze(text)`

Tier 1 only — useful for debugging pattern hits without full tool-result traversal.
Expand Down
16 changes: 15 additions & 1 deletion src/stackone_defender/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

from .classifiers.onnx_classifier import get_default_model_path
from .classifiers.tier3_orchestrator import get_default_tier3_provider, set_default_tier3_provider
from .core.prompt_defense import PromptDefense, create_prompt_defense
from .sfe.preprocess import (
DropDecision,
Expand All @@ -21,10 +22,19 @@
get_default_sfe_model_path,
sfe_preprocess,
)
from .types import DefenseResult, MultiheadConfig, RiskLevel, Tier1Result
from .types import (
DefenderMode,
DefenseResult,
MultiheadConfig,
RiskLevel,
Tier1Result,
Tier3Provider,
Tier3Verdict,
)
from .utils.boundary import contains_boundary_patterns, generate_boundary_instructions

__all__ = [
"DefenderMode",
"DefenseResult",
"DropDecision",
"MultiheadConfig",
Expand All @@ -33,11 +43,15 @@
"SfePredictor",
"SfePreprocessResult",
"Tier1Result",
"Tier3Provider",
"Tier3Verdict",
"contains_boundary_patterns",
"create_prompt_defense",
"generate_boundary_instructions",
"get_default_model_path",
"get_default_predictor",
"get_default_sfe_model_path",
"get_default_tier3_provider",
"set_default_tier3_provider",
"sfe_preprocess",
]
27 changes: 27 additions & 0 deletions src/stackone_defender/classifiers/tier3_orchestrator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Tier 3 provider registry.

The defender package ships no Tier 3 implementations — proprietary model
endpoints (SageMaker, OpenAI, etc.) live in consumer code. Consumers call
``set_default_tier3_provider(provider)`` once at app startup; ``PromptDefense``
picks the registered provider up when callers opt in via ``enable_tier3=True``.

Module-level singleton because the defender is often instantiated per-request
and we don't want to pipe a provider object through that boundary on every call.
"""

from __future__ import annotations

from ..types import Tier3Provider

_default_provider: Tier3Provider | None = None


def set_default_tier3_provider(provider: Tier3Provider | None) -> None:
"""Register the process-wide default Tier 3 provider. Pass ``None`` to clear."""
global _default_provider
_default_provider = provider


def get_default_tier3_provider() -> Tier3Provider | None:
"""Return the registered default Tier 3 provider, or ``None`` if unset."""
return _default_provider
Loading
Loading