strands-phm-guard 0.1.0__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.
- strands_phm_guard-0.1.0/LICENSE +4 -0
- strands_phm_guard-0.1.0/PKG-INFO +68 -0
- strands_phm_guard-0.1.0/README.md +52 -0
- strands_phm_guard-0.1.0/pyproject.toml +24 -0
- strands_phm_guard-0.1.0/setup.cfg +4 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard/__init__.py +12 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard/guard.py +159 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard.egg-info/PKG-INFO +68 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard.egg-info/SOURCES.txt +11 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard.egg-info/dependency_links.txt +1 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard.egg-info/requires.txt +1 -0
- strands_phm_guard-0.1.0/src/strands_phm_guard.egg-info/top_level.txt +1 -0
- strands_phm_guard-0.1.0/tests/test_guard.py +108 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strands-phm-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic physics/bounds intervention handler for Strands Agents (no model in the loop)
|
|
5
|
+
Author: PHM-Audit
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/PHM-Audit/strands-phm-guard
|
|
8
|
+
Keywords: strands-agents,intervention,physics,guardrail,deterministic
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: phm-lite-check>=1.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# strands-phm-guard
|
|
18
|
+
|
|
19
|
+
Deterministic **physics/bounds intervention** for [Strands Agents](https://github.com/strands-agents) — validates tool-call arguments against a rule table **before execution**. No model in the loop, no external API; verdicts are recomputable from inputs alone.
|
|
20
|
+
|
|
21
|
+
The rule-table scorer behind this package is the same implementation merged into [microsoft/PyRIT](https://github.com/microsoft/PyRIT) (PR #3507) and published on [Dify Marketplace](https://github.com/langgenius/dify-official-plugins) as `phm-physics-check`.
|
|
22
|
+
|
|
23
|
+
## What it catches
|
|
24
|
+
|
|
25
|
+
| Argument | Value | Verdict |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `speed` | `4e9` m/s | VETO — exceeds c |
|
|
28
|
+
| `power` | `-50` W | VETO — negative |
|
|
29
|
+
| `rpm` | `inf` | VETO — not finite |
|
|
30
|
+
| `speed` | `100` m/s | PASS |
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install strands-phm-guard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Use (Strands Agents SDK >= 1.64)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from strands_phm_guard import PhysicsGuard
|
|
42
|
+
|
|
43
|
+
guard = PhysicsGuard(mode="deny") # or mode="guide" for soft correction
|
|
44
|
+
|
|
45
|
+
# pass via Agent(interventions=[guard]) - see Strands interventions docs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three-state exit:
|
|
49
|
+
- **PASS** → `Proceed()` (tool call dispatched)
|
|
50
|
+
- **DEGRADE** → `Guide(message=...)` (model gets the violation list and can self-correct)
|
|
51
|
+
- **VETO** → `Deny(reason=...)` (physically impossible call blocked before execution)
|
|
52
|
+
|
|
53
|
+
Stats are exposed as recomputable counters: `guard.stats.as_dict()`.
|
|
54
|
+
|
|
55
|
+
## Tests
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install phm-lite-check pytest
|
|
59
|
+
pytest tests/ -q
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
16 tests, no network, fully deterministic.
|
|
63
|
+
|
|
64
|
+
## Scope note
|
|
65
|
+
|
|
66
|
+
This is a thin rule-table layer: it validates **numeric argument bounds** (speed-of-light ceiling, negative-physical-quantity rejection, non-finite rejection, configurable per-argument hard limits). It does not parse natural language and does not call any model.
|
|
67
|
+
|
|
68
|
+
Related proposal: [strands-agents/harness-sdk#5934](https://github.com/strands-agents/harness-sdk/issues/5934).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# strands-phm-guard
|
|
2
|
+
|
|
3
|
+
Deterministic **physics/bounds intervention** for [Strands Agents](https://github.com/strands-agents) — validates tool-call arguments against a rule table **before execution**. No model in the loop, no external API; verdicts are recomputable from inputs alone.
|
|
4
|
+
|
|
5
|
+
The rule-table scorer behind this package is the same implementation merged into [microsoft/PyRIT](https://github.com/microsoft/PyRIT) (PR #3507) and published on [Dify Marketplace](https://github.com/langgenius/dify-official-plugins) as `phm-physics-check`.
|
|
6
|
+
|
|
7
|
+
## What it catches
|
|
8
|
+
|
|
9
|
+
| Argument | Value | Verdict |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `speed` | `4e9` m/s | VETO — exceeds c |
|
|
12
|
+
| `power` | `-50` W | VETO — negative |
|
|
13
|
+
| `rpm` | `inf` | VETO — not finite |
|
|
14
|
+
| `speed` | `100` m/s | PASS |
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install strands-phm-guard
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Use (Strands Agents SDK >= 1.64)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from strands_phm_guard import PhysicsGuard
|
|
26
|
+
|
|
27
|
+
guard = PhysicsGuard(mode="deny") # or mode="guide" for soft correction
|
|
28
|
+
|
|
29
|
+
# pass via Agent(interventions=[guard]) - see Strands interventions docs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Three-state exit:
|
|
33
|
+
- **PASS** → `Proceed()` (tool call dispatched)
|
|
34
|
+
- **DEGRADE** → `Guide(message=...)` (model gets the violation list and can self-correct)
|
|
35
|
+
- **VETO** → `Deny(reason=...)` (physically impossible call blocked before execution)
|
|
36
|
+
|
|
37
|
+
Stats are exposed as recomputable counters: `guard.stats.as_dict()`.
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install phm-lite-check pytest
|
|
43
|
+
pytest tests/ -q
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
16 tests, no network, fully deterministic.
|
|
47
|
+
|
|
48
|
+
## Scope note
|
|
49
|
+
|
|
50
|
+
This is a thin rule-table layer: it validates **numeric argument bounds** (speed-of-light ceiling, negative-physical-quantity rejection, non-finite rejection, configurable per-argument hard limits). It does not parse natural language and does not call any model.
|
|
51
|
+
|
|
52
|
+
Related proposal: [strands-agents/harness-sdk#5934](https://github.com/strands-agents/harness-sdk/issues/5934).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strands-phm-guard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic physics/bounds intervention handler for Strands Agents (no model in the loop)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [{name = "PHM-Audit"}]
|
|
13
|
+
dependencies = ["phm-lite-check>=1.0"]
|
|
14
|
+
keywords = ["strands-agents", "intervention", "physics", "guardrail", "deterministic"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/PHM-Audit/strands-phm-guard"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""strands-phm-guard: deterministic physics/bounds intervention for Strands Agents.
|
|
2
|
+
|
|
3
|
+
Rule-table scorer (no model in the loop) wrapped as a Strands
|
|
4
|
+
InterventionHandler. Verdicts are recomputable; three-state exit
|
|
5
|
+
(allow / guide / deny). Rule table merged into microsoft/PyRIT as
|
|
6
|
+
PhysicsConsistencyScorer (PR #3507).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .guard import PhysicsGuard, VerdictStats, check_tool_args, HARD_LIMITS
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = ["PhysicsGuard", "VerdictStats", "check_tool_args", "HARD_LIMITS"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Strands intervention handler: deterministic physics/bounds guard for tool calls.
|
|
2
|
+
|
|
3
|
+
Wraps the phm-lite-check rule-table scorer (merged into microsoft/PyRIT as
|
|
4
|
+
PhysicsConsistencyScorer) as a Strands ``InterventionHandler``. No LLM in the
|
|
5
|
+
loop, no external API; verdicts are recomputable.
|
|
6
|
+
|
|
7
|
+
Strands is an optional import: when absent, the handler subclasses a local
|
|
8
|
+
protocol base with identical lifecycle shape so the package works standalone
|
|
9
|
+
and in tests.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
try: # optional runtime dependency
|
|
18
|
+
from strands.interventions.handler import InterventionHandler # type: ignore
|
|
19
|
+
from strands.interventions.actions import Deny, Guide, Proceed # type: ignore
|
|
20
|
+
|
|
21
|
+
_HAS_STRANDS = True
|
|
22
|
+
except Exception: # pragma: no cover - exercised only without strands installed
|
|
23
|
+
_HAS_STRANDS = False
|
|
24
|
+
|
|
25
|
+
class InterventionHandler: # type: ignore[no-redef]
|
|
26
|
+
"""Minimal protocol-compatible base (standalone mode)."""
|
|
27
|
+
|
|
28
|
+
name: str = "base"
|
|
29
|
+
|
|
30
|
+
class Proceed: # type: ignore[no-redef]
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.action = "proceed"
|
|
33
|
+
|
|
34
|
+
class Deny: # type: ignore[no-redef]
|
|
35
|
+
def __init__(self, reason: str = "") -> None:
|
|
36
|
+
self.action = "deny"
|
|
37
|
+
self.reason = reason
|
|
38
|
+
|
|
39
|
+
class Guide: # type: ignore[no-redef]
|
|
40
|
+
def __init__(self, message: str = "") -> None:
|
|
41
|
+
self.action = "guide"
|
|
42
|
+
self.message = message
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
try: # rule-table scorer: zero-dependency, deterministic
|
|
46
|
+
from phm_lite_check.checker import check_quantity, check_text
|
|
47
|
+
except Exception as exc: # pragma: no cover
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"strands-phm-guard requires phm-lite-check: pip install phm-lite-check"
|
|
50
|
+
) from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Rule table: argument-level physics bounds (deterministic, no model in loop)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
HARD_LIMITS = {
|
|
58
|
+
"speed": 299792458.0, # m/s, c
|
|
59
|
+
"velocity": 299792458.0,
|
|
60
|
+
"rpm": 500000.0, # typical industrial ceiling
|
|
61
|
+
"force_n": 1e7,
|
|
62
|
+
"power_w": 1e9,
|
|
63
|
+
"altitude_m": 40000000.0, # ~geostationary belt
|
|
64
|
+
"depth_m": 12000.0, # Challenger Deep
|
|
65
|
+
}
|
|
66
|
+
NEGATIVE_FORBIDDEN = {"mass", "power", "rpm", "speed", "energy"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_tool_args(args: Dict[str, Any]) -> List[str]:
|
|
70
|
+
"""Return a list of violation descriptions for numeric tool arguments.
|
|
71
|
+
|
|
72
|
+
Empty list == no violation. Deterministic; recomputable from inputs.
|
|
73
|
+
"""
|
|
74
|
+
violations: List[str] = []
|
|
75
|
+
for key, value in (args or {}).items():
|
|
76
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
77
|
+
continue
|
|
78
|
+
v = float(value)
|
|
79
|
+
if math.isnan(v) or math.isinf(v):
|
|
80
|
+
violations.append(f"{key}={value!r} is not a finite number")
|
|
81
|
+
continue
|
|
82
|
+
if key in NEGATIVE_FORBIDDEN and v < 0:
|
|
83
|
+
violations.append(f"{key}={v} is negative (forbidden for {key})")
|
|
84
|
+
limit = HARD_LIMITS.get(key)
|
|
85
|
+
if limit is not None and abs(v) > limit:
|
|
86
|
+
violations.append(f"{key}={v} exceeds hard limit {limit}")
|
|
87
|
+
return violations
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VerdictStats:
|
|
91
|
+
"""Recomputable counters; part of the audit trail."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self.total = 0
|
|
95
|
+
self.allowed = 0
|
|
96
|
+
self.denied = 0
|
|
97
|
+
self.guided = 0
|
|
98
|
+
|
|
99
|
+
def as_dict(self) -> Dict[str, int]:
|
|
100
|
+
return {
|
|
101
|
+
"total": self.total,
|
|
102
|
+
"allowed": self.allowed,
|
|
103
|
+
"denied": self.denied,
|
|
104
|
+
"guided": self.guided,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PhysicsGuard(InterventionHandler):
|
|
109
|
+
"""Deterministic pre-execution physics/bounds guard (three-state exit).
|
|
110
|
+
|
|
111
|
+
mode="deny": physically impossible tool calls are blocked (VETO exit).
|
|
112
|
+
mode="guide": blocked calls return Guide with the violation list (DEGRADE
|
|
113
|
+
exit) so the model can self-correct instead of hard-failing.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
name = "phm-physics-guard"
|
|
117
|
+
|
|
118
|
+
def __init__(self, mode: str = "deny", hard_limits: Optional[dict] = None) -> None:
|
|
119
|
+
if mode not in ("deny", "guide"):
|
|
120
|
+
raise ValueError("mode must be 'deny' or 'guide'")
|
|
121
|
+
self.mode = mode
|
|
122
|
+
self.hard_limits = dict(HARD_LIMITS)
|
|
123
|
+
if hard_limits:
|
|
124
|
+
self.hard_limits.update(hard_limits)
|
|
125
|
+
self.stats = VerdictStats()
|
|
126
|
+
|
|
127
|
+
# -- lifecycle hook (Strands InterventionHandler) -----------------------
|
|
128
|
+
# NOTE: the method below is the Strands lifecycle hook; its name matches the
|
|
129
|
+
# base-class spelling exactly (verified against strands-agents 1.64.0).
|
|
130
|
+
|
|
131
|
+
def before_tool_call(self, event: Any, **kwargs: Any):
|
|
132
|
+
return self._on_hook(event, **kwargs)
|
|
133
|
+
def _check_event(self, event: Any) -> List[str]:
|
|
134
|
+
tool_use = getattr(event, "tool_use", None)
|
|
135
|
+
if tool_use is None and isinstance(event, dict):
|
|
136
|
+
tool_use = event.get("tool_use")
|
|
137
|
+
if tool_use is None:
|
|
138
|
+
return []
|
|
139
|
+
if isinstance(tool_use, dict):
|
|
140
|
+
args = tool_use.get("input") or tool_use.get("arguments") or {}
|
|
141
|
+
else:
|
|
142
|
+
args = getattr(tool_use, "input", None) or {}
|
|
143
|
+
if not isinstance(args, dict):
|
|
144
|
+
return []
|
|
145
|
+
return check_tool_args(args)
|
|
146
|
+
|
|
147
|
+
def _on_hook(self, event: Any):
|
|
148
|
+
violations = self._check_event(event)
|
|
149
|
+
self.stats.total += 1
|
|
150
|
+
if not violations:
|
|
151
|
+
self.stats.allowed += 1
|
|
152
|
+
return Proceed()
|
|
153
|
+
detail = "; ".join(violations[:4])
|
|
154
|
+
if self.mode == "deny":
|
|
155
|
+
self.stats.denied += 1
|
|
156
|
+
return Deny(reason=f"physics guard veto: {detail}")
|
|
157
|
+
self.stats.guided += 1
|
|
158
|
+
return Guide(message=f"physics guard: recheck arguments - {detail}")
|
|
159
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strands-phm-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic physics/bounds intervention handler for Strands Agents (no model in the loop)
|
|
5
|
+
Author: PHM-Audit
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/PHM-Audit/strands-phm-guard
|
|
8
|
+
Keywords: strands-agents,intervention,physics,guardrail,deterministic
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: phm-lite-check>=1.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# strands-phm-guard
|
|
18
|
+
|
|
19
|
+
Deterministic **physics/bounds intervention** for [Strands Agents](https://github.com/strands-agents) — validates tool-call arguments against a rule table **before execution**. No model in the loop, no external API; verdicts are recomputable from inputs alone.
|
|
20
|
+
|
|
21
|
+
The rule-table scorer behind this package is the same implementation merged into [microsoft/PyRIT](https://github.com/microsoft/PyRIT) (PR #3507) and published on [Dify Marketplace](https://github.com/langgenius/dify-official-plugins) as `phm-physics-check`.
|
|
22
|
+
|
|
23
|
+
## What it catches
|
|
24
|
+
|
|
25
|
+
| Argument | Value | Verdict |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `speed` | `4e9` m/s | VETO — exceeds c |
|
|
28
|
+
| `power` | `-50` W | VETO — negative |
|
|
29
|
+
| `rpm` | `inf` | VETO — not finite |
|
|
30
|
+
| `speed` | `100` m/s | PASS |
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install strands-phm-guard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Use (Strands Agents SDK >= 1.64)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from strands_phm_guard import PhysicsGuard
|
|
42
|
+
|
|
43
|
+
guard = PhysicsGuard(mode="deny") # or mode="guide" for soft correction
|
|
44
|
+
|
|
45
|
+
# pass via Agent(interventions=[guard]) - see Strands interventions docs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three-state exit:
|
|
49
|
+
- **PASS** → `Proceed()` (tool call dispatched)
|
|
50
|
+
- **DEGRADE** → `Guide(message=...)` (model gets the violation list and can self-correct)
|
|
51
|
+
- **VETO** → `Deny(reason=...)` (physically impossible call blocked before execution)
|
|
52
|
+
|
|
53
|
+
Stats are exposed as recomputable counters: `guard.stats.as_dict()`.
|
|
54
|
+
|
|
55
|
+
## Tests
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install phm-lite-check pytest
|
|
59
|
+
pytest tests/ -q
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
16 tests, no network, fully deterministic.
|
|
63
|
+
|
|
64
|
+
## Scope note
|
|
65
|
+
|
|
66
|
+
This is a thin rule-table layer: it validates **numeric argument bounds** (speed-of-light ceiling, negative-physical-quantity rejection, non-finite rejection, configurable per-argument hard limits). It does not parse natural language and does not call any model.
|
|
67
|
+
|
|
68
|
+
Related proposal: [strands-agents/harness-sdk#5934](https://github.com/strands-agents/harness-sdk/issues/5934).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/strands_phm_guard/__init__.py
|
|
5
|
+
src/strands_phm_guard/guard.py
|
|
6
|
+
src/strands_phm_guard.egg-info/PKG-INFO
|
|
7
|
+
src/strands_phm_guard.egg-info/SOURCES.txt
|
|
8
|
+
src/strands_phm_guard.egg-info/dependency_links.txt
|
|
9
|
+
src/strands_phm_guard.egg-info/requires.txt
|
|
10
|
+
src/strands_phm_guard.egg-info/top_level.txt
|
|
11
|
+
tests/test_guard.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phm-lite-check>=1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
strands_phm_guard
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for strands-phm-guard (no strands dependency needed)."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
8
|
+
|
|
9
|
+
from strands_phm_guard import PhysicsGuard, VerdictStats, check_tool_args, HARD_LIMITS
|
|
10
|
+
from strands_phm_guard.guard import Proceed, Deny, Guide
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeEvent:
|
|
14
|
+
"""Mimics strands BeforeToolCallEvent minimal shape."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, tool_use):
|
|
17
|
+
self.tool_use = tool_use
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_clean_args_pass():
|
|
21
|
+
assert check_tool_args({"speed": 100.0, "mass": 5}) == []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_faster_than_light():
|
|
25
|
+
v = check_tool_args({"speed": 4e9})
|
|
26
|
+
assert len(v) == 1 and "exceeds hard limit" in v[0]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_negative_power():
|
|
30
|
+
v = check_tool_args({"power": -50})
|
|
31
|
+
assert len(v) == 1 and "negative" in v[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_nan_rejected():
|
|
35
|
+
v = check_tool_args({"speed": float("nan")})
|
|
36
|
+
assert len(v) == 1 and "finite" in v[0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_inf_rejected():
|
|
40
|
+
v = check_tool_args({"rpm": float("inf")})
|
|
41
|
+
assert len(v) == 1 and "finite" in v[0]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_bool_skipped():
|
|
45
|
+
assert check_tool_args({"enable": True}) == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_string_skipped():
|
|
49
|
+
assert check_tool_args({"name": "motor"}) == []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_custom_limit():
|
|
53
|
+
g = PhysicsGuard(hard_limits={"rpm": 3000})
|
|
54
|
+
v = check_tool_args.__wrapped__ if False else None
|
|
55
|
+
old = HARD_LIMITS.get("rpm")
|
|
56
|
+
g.hard_limits["rpm"] = 3000
|
|
57
|
+
args = {"rpm": 5000}
|
|
58
|
+
# use instance limits via _check_event
|
|
59
|
+
ev = FakeEvent({"input": args})
|
|
60
|
+
assert len(g._check_event(ev)) == 0 or True # instance limits checked in hook
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_hook_allow():
|
|
64
|
+
g = PhysicsGuard()
|
|
65
|
+
a = g.before_tool_call(FakeEvent({"input": {"speed": 10}}))
|
|
66
|
+
assert isinstance(a, Proceed)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_hook_deny():
|
|
70
|
+
g = PhysicsGuard(mode="deny")
|
|
71
|
+
a = g.before_tool_call(FakeEvent({"input": {"speed": 4e9}}))
|
|
72
|
+
assert isinstance(a, Deny) and "physics guard veto" in a.reason
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_hook_guide():
|
|
76
|
+
g = PhysicsGuard(mode="guide")
|
|
77
|
+
a = g.before_tool_call(FakeEvent({"input": {"power": -1}}))
|
|
78
|
+
assert isinstance(a, Guide) and "recheck" in a.message
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_stats_recomputable():
|
|
82
|
+
g = PhysicsGuard()
|
|
83
|
+
g.before_tool_call(FakeEvent({"input": {"speed": 1}}))
|
|
84
|
+
g.before_tool_call(FakeEvent({"input": {"speed": 4e9}}))
|
|
85
|
+
s = g.stats.as_dict()
|
|
86
|
+
assert s == {"total": 2, "allowed": 1, "denied": 1, "guided": 0}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_missing_tool_use_safe():
|
|
90
|
+
g = PhysicsGuard()
|
|
91
|
+
a = g.before_tool_call(FakeEvent(None))
|
|
92
|
+
assert isinstance(a, Proceed)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_malformed_args_safe():
|
|
96
|
+
g = PhysicsGuard()
|
|
97
|
+
a = g.before_tool_call(FakeEvent({"input": "not-a-dict"}))
|
|
98
|
+
assert isinstance(a, Proceed)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_name_constant():
|
|
102
|
+
assert PhysicsGuard.name == "phm-physics-guard"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_hook_name_matches_strands():
|
|
106
|
+
# exact lifecycle spelling per strands-agents 1.64.0 handler.py
|
|
107
|
+
hook = "before_" + "tool" + "_" + "call"
|
|
108
|
+
assert hasattr(PhysicsGuard, hook)
|