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.
Files changed (56) hide show
  1. stackone_defender-0.7.2/.release-please-manifest.json +1 -0
  2. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/CHANGELOG.md +8 -0
  3. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/PKG-INFO +1 -1
  4. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/pyproject.toml +1 -1
  5. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/__init__.py +2 -0
  6. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/prompt_defense.py +17 -0
  7. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/types.py +10 -0
  8. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_tier3.py +81 -2
  9. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/uv.lock +1 -1
  10. stackone_defender-0.7.1/.release-please-manifest.json +0 -1
  11. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.github/workflows/ci.yaml +0 -0
  12. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.github/workflows/release.yaml +0 -0
  13. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.gitignore +0 -0
  14. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.python-version +0 -0
  15. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/.release-please-config.json +0 -0
  16. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/README.md +0 -0
  17. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/config.json +0 -0
  18. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/model_quantized.onnx +0 -0
  19. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/tokenizer.json +0 -0
  20. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/models/minilm-full-aug/tokenizer_config.json +0 -0
  21. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/__init__.py +0 -0
  22. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/onnx_classifier.py +0 -0
  23. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/pattern_detector.py +0 -0
  24. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/patterns.py +0 -0
  25. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/tier2_classifier.py +0 -0
  26. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/classifiers/tier3_orchestrator.py +0 -0
  27. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/config.py +0 -0
  28. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/__init__.py +0 -0
  29. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/core/tool_result_sanitizer.py +0 -0
  30. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/classifier_config.json +0 -0
  31. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/config.json +0 -0
  32. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/model_quantized.onnx +0 -0
  33. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/tokenizer.json +0 -0
  34. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/models/minilm-multihead-v5/tokenizer_config.json +0 -0
  35. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/__init__.py +0 -0
  36. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/encoding_detector.py +0 -0
  37. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/leet_normalizer.py +0 -0
  38. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/normalizer.py +0 -0
  39. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/pattern_remover.py +0 -0
  40. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/role_stripper.py +0 -0
  41. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sanitizers/sanitizer.py +0 -0
  42. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/__init__.py +0 -0
  43. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/model.ftz +0 -0
  44. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/sfe/preprocess.py +0 -0
  45. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/__init__.py +0 -0
  46. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/boundary.py +0 -0
  47. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/field_detection.py +0 -0
  48. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/src/stackone_defender/utils/structure.py +0 -0
  49. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/__init__.py +0 -0
  50. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_integration.py +0 -0
  51. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_onnx_classifier.py +0 -0
  52. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_pattern_detector.py +0 -0
  53. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_sanitizers.py +0 -0
  54. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_sfe.py +0 -0
  55. {stackone_defender-0.7.1 → stackone_defender-0.7.2}/tests/test_tier2_classifier.py +0 -0
  56. {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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackone-defender"
3
- version = "0.7.1"
3
+ version = "0.7.2"
4
4
  description = "Indirect prompt injection defense for AI agents using tool calls"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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",
@@ -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
@@ -493,7 +493,7 @@ wheels = [
493
493
 
494
494
  [[package]]
495
495
  name = "stackone-defender"
496
- version = "0.7.0"
496
+ version = "0.7.1"
497
497
  source = { editable = "." }
498
498
 
499
499
  [package.optional-dependencies]
@@ -1 +0,0 @@
1
- {".":"0.7.1"}