stackone-defender 0.7.1__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackone_defender-0.7.2/.release-please-manifest.json +1 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/CHANGELOG.md +8 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/PKG-INFO +1 -1
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/pyproject.toml +1 -1
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/__init__.py +2 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/prompt_defense.py +17 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/types.py +10 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_tier3.py +81 -2
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/uv.lock +1 -1
- stackone_defender-0.7.1/.release-please-manifest.json +0 -1
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.github/workflows/ci.yaml +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.github/workflows/release.yaml +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.gitignore +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.python-version +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.release-please-config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/README.md +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/model_quantized.onnx +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/tokenizer.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/tokenizer_config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/onnx_classifier.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/pattern_detector.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/patterns.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/tier2_classifier.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/tier3_orchestrator.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/config.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/tool_result_sanitizer.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/classifier_config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/model_quantized.onnx +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/tokenizer.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/tokenizer_config.json +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/encoding_detector.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/leet_normalizer.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/normalizer.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/pattern_remover.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/role_stripper.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/sanitizer.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/model.ftz +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/preprocess.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/boundary.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/field_detection.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/structure.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/__init__.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_integration.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_onnx_classifier.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_pattern_detector.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_sanitizers.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_sfe.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_tier2_classifier.py +0 -0
- {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{".":"0.7.2"}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.2](https://github.com/StackOneHQ/stackone-defender/compare/stackone-defender-v0.7.1...stackone-defender-v0.7.2) (2026-06-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add Tier3Verdict.usage for TS 0.7.2 parity ([2e18269](https://github.com/StackOneHQ/stackone-defender/commit/2e182695c20e9b087041e55f7466cca9b659521a))
|
|
9
|
+
* Tier3Verdict.usage parity with @stackone/defender 0.7.2 ([1c9c0c8](https://github.com/StackOneHQ/stackone-defender/commit/1c9c0c8026fd8c4dda2f78c31a1f500c01045d43))
|
|
10
|
+
|
|
3
11
|
## [0.7.1](https://github.com/StackOneHQ/stackone-defender/compare/stackone-defender-v0.7.0...stackone-defender-v0.7.1) (2026-06-16)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackone-defender
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Indirect prompt injection defense for AI agents using tool calls
|
|
5
5
|
Project-URL: Homepage, https://github.com/StackOneHQ/stackone-defender
|
|
6
6
|
Project-URL: Repository, https://github.com/StackOneHQ/stackone-defender
|
|
@@ -29,6 +29,7 @@ from .types import (
|
|
|
29
29
|
RiskLevel,
|
|
30
30
|
Tier1Result,
|
|
31
31
|
Tier3Provider,
|
|
32
|
+
Tier3TokenUsage,
|
|
32
33
|
Tier3Verdict,
|
|
33
34
|
)
|
|
34
35
|
from .utils.boundary import contains_boundary_patterns, generate_boundary_instructions
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"SfePreprocessResult",
|
|
45
46
|
"Tier1Result",
|
|
46
47
|
"Tier3Provider",
|
|
48
|
+
"Tier3TokenUsage",
|
|
47
49
|
"Tier3Verdict",
|
|
48
50
|
"contains_boundary_patterns",
|
|
49
51
|
"create_prompt_defense",
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/prompt_defense.py
RENAMED
|
@@ -30,6 +30,7 @@ from ..types import (
|
|
|
30
30
|
Tier3Provider,
|
|
31
31
|
Tier3Result,
|
|
32
32
|
Tier3Skip,
|
|
33
|
+
Tier3TokenUsage,
|
|
33
34
|
Tier3Verdict,
|
|
34
35
|
)
|
|
35
36
|
from .tool_result_sanitizer import ToolResultSanitizer, create_tool_result_sanitizer
|
|
@@ -292,6 +293,21 @@ class PromptDefense:
|
|
|
292
293
|
def _resolve_tier3_provider(self) -> Tier3Provider | None:
|
|
293
294
|
return self._tier3_custom_provider or get_default_tier3_provider()
|
|
294
295
|
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _parse_tier3_usage(usage: Any) -> Tier3TokenUsage | None:
|
|
298
|
+
if usage is None or not isinstance(usage, dict):
|
|
299
|
+
return None
|
|
300
|
+
prompt_tokens = usage.get("prompt_tokens", usage.get("promptTokens"))
|
|
301
|
+
completion_tokens = usage.get("completion_tokens", usage.get("completionTokens"))
|
|
302
|
+
total_tokens = usage.get("total_tokens", usage.get("totalTokens"))
|
|
303
|
+
if not all(isinstance(value, int) for value in (prompt_tokens, completion_tokens, total_tokens)):
|
|
304
|
+
return None
|
|
305
|
+
return Tier3TokenUsage(
|
|
306
|
+
prompt_tokens=prompt_tokens,
|
|
307
|
+
completion_tokens=completion_tokens,
|
|
308
|
+
total_tokens=total_tokens,
|
|
309
|
+
)
|
|
310
|
+
|
|
295
311
|
@staticmethod
|
|
296
312
|
def _validate_tier3_verdict(verdict: Any) -> Tier3Verdict | Tier3Skip:
|
|
297
313
|
if isinstance(verdict, Tier3Verdict):
|
|
@@ -317,6 +333,7 @@ class PromptDefense:
|
|
|
317
333
|
score=verdict.get("score"),
|
|
318
334
|
raw=verdict.get("raw"),
|
|
319
335
|
latency_ms=verdict.get("latency_ms", verdict.get("latencyMs")),
|
|
336
|
+
usage=PromptDefense._parse_tier3_usage(verdict.get("usage")),
|
|
320
337
|
)
|
|
321
338
|
|
|
322
339
|
@staticmethod
|
|
@@ -65,6 +65,15 @@ class Tier1Result:
|
|
|
65
65
|
latency_ms: float
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
@dataclass
|
|
69
|
+
class Tier3TokenUsage:
|
|
70
|
+
"""Token usage reported by a Tier 3 provider (e.g. vLLM or OpenAI ``usage``)."""
|
|
71
|
+
|
|
72
|
+
prompt_tokens: int
|
|
73
|
+
completion_tokens: int
|
|
74
|
+
total_tokens: int
|
|
75
|
+
|
|
76
|
+
|
|
68
77
|
@dataclass
|
|
69
78
|
class Tier3Verdict:
|
|
70
79
|
"""Authoritative block/allow decision from a Tier 3 provider."""
|
|
@@ -73,6 +82,7 @@ class Tier3Verdict:
|
|
|
73
82
|
score: float | None = None
|
|
74
83
|
raw: Any = None
|
|
75
84
|
latency_ms: float | None = None
|
|
85
|
+
usage: Tier3TokenUsage | None = None
|
|
76
86
|
|
|
77
87
|
|
|
78
88
|
@dataclass
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ from stackone_defender import (
|
|
|
12
12
|
get_default_tier3_provider,
|
|
13
13
|
set_default_tier3_provider,
|
|
14
14
|
)
|
|
15
|
-
from stackone_defender.types import Tier3Skip, Tier3Verdict
|
|
15
|
+
from stackone_defender.types import Tier3Skip, Tier3TokenUsage, Tier3Verdict
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _make_provider(decision: str) -> MagicMock:
|
|
@@ -378,3 +378,82 @@ class TestDefendToolResultsAsync:
|
|
|
378
378
|
results = defense.defend_tool_results(items)
|
|
379
379
|
assert len(results) == 1
|
|
380
380
|
assert isinstance(results[0].tier3, Tier3Verdict)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class TestTier3UsagePropagation:
|
|
384
|
+
def test_passes_provider_reported_usage_through_to_result_tier3(self):
|
|
385
|
+
provider = MagicMock()
|
|
386
|
+
provider.classify = AsyncMock(
|
|
387
|
+
return_value={
|
|
388
|
+
"decision": "allow",
|
|
389
|
+
"latencyMs": 42,
|
|
390
|
+
"usage": {
|
|
391
|
+
"promptTokens": 311,
|
|
392
|
+
"completionTokens": 17,
|
|
393
|
+
"totalTokens": 328,
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
defense = create_prompt_defense(
|
|
398
|
+
enable_tier1=False,
|
|
399
|
+
enable_tier2=False,
|
|
400
|
+
enable_tier3=True,
|
|
401
|
+
defender_mode="tier3_only",
|
|
402
|
+
tier3={"provider": provider},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
result = asyncio.run(defense.defend_tool_result_async({"body": "test"}, "test_tool"))
|
|
406
|
+
|
|
407
|
+
assert isinstance(result.tier3, Tier3Verdict)
|
|
408
|
+
assert result.tier3.usage == Tier3TokenUsage(
|
|
409
|
+
prompt_tokens=311,
|
|
410
|
+
completion_tokens=17,
|
|
411
|
+
total_tokens=328,
|
|
412
|
+
)
|
|
413
|
+
assert result.tier3.latency_ms == 42
|
|
414
|
+
|
|
415
|
+
def test_passes_snake_case_usage_through_public_api(self):
|
|
416
|
+
provider = MagicMock()
|
|
417
|
+
provider.classify = AsyncMock(
|
|
418
|
+
return_value={
|
|
419
|
+
"decision": "allow",
|
|
420
|
+
"usage": {
|
|
421
|
+
"prompt_tokens": 10,
|
|
422
|
+
"completion_tokens": 5,
|
|
423
|
+
"total_tokens": 15,
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
defense = create_prompt_defense(
|
|
428
|
+
enable_tier1=False,
|
|
429
|
+
enable_tier2=False,
|
|
430
|
+
enable_tier3=True,
|
|
431
|
+
defender_mode="tier3_only",
|
|
432
|
+
tier3={"provider": provider},
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
result = asyncio.run(defense.defend_tool_result_async({"body": "test"}, "test_tool"))
|
|
436
|
+
|
|
437
|
+
assert isinstance(result.tier3, Tier3Verdict)
|
|
438
|
+
assert result.tier3.usage == Tier3TokenUsage(
|
|
439
|
+
prompt_tokens=10,
|
|
440
|
+
completion_tokens=5,
|
|
441
|
+
total_tokens=15,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def test_preserves_usage_when_provider_returns_tier3_verdict_instance(self):
|
|
445
|
+
usage = Tier3TokenUsage(prompt_tokens=1, completion_tokens=2, total_tokens=3)
|
|
446
|
+
provider = MagicMock()
|
|
447
|
+
provider.classify = AsyncMock(return_value=Tier3Verdict(decision="allow", usage=usage))
|
|
448
|
+
defense = create_prompt_defense(
|
|
449
|
+
enable_tier1=False,
|
|
450
|
+
enable_tier2=False,
|
|
451
|
+
enable_tier3=True,
|
|
452
|
+
defender_mode="tier3_only",
|
|
453
|
+
tier3={"provider": provider},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
result = asyncio.run(defense.defend_tool_result_async({"body": "test"}, "test_tool"))
|
|
457
|
+
|
|
458
|
+
assert isinstance(result.tier3, Tier3Verdict)
|
|
459
|
+
assert result.tier3.usage == usage
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{".":"0.7.1"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/model_quantized.onnx
RENAMED
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/tokenizer_config.json
RENAMED
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/patterns.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/normalizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/sanitizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/field_detection.py
RENAMED
|
File without changes
|
{stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/structure.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|