iris-security-anthropic 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.
- iris_security_anthropic-0.1.0/PKG-INFO +50 -0
- iris_security_anthropic-0.1.0/README.md +31 -0
- iris_security_anthropic-0.1.0/examples/governed_claude.py +31 -0
- iris_security_anthropic-0.1.0/iris_anthropic/__init__.py +43 -0
- iris_security_anthropic-0.1.0/iris_anthropic/_governance.py +144 -0
- iris_security_anthropic-0.1.0/iris_anthropic/client.py +224 -0
- iris_security_anthropic-0.1.0/iris_anthropic/guardrails.py +101 -0
- iris_security_anthropic-0.1.0/iris_security_anthropic.egg-info/PKG-INFO +50 -0
- iris_security_anthropic-0.1.0/iris_security_anthropic.egg-info/SOURCES.txt +14 -0
- iris_security_anthropic-0.1.0/iris_security_anthropic.egg-info/dependency_links.txt +1 -0
- iris_security_anthropic-0.1.0/iris_security_anthropic.egg-info/requires.txt +10 -0
- iris_security_anthropic-0.1.0/iris_security_anthropic.egg-info/top_level.txt +4 -0
- iris_security_anthropic-0.1.0/pyproject.toml +39 -0
- iris_security_anthropic-0.1.0/setup.cfg +4 -0
- iris_security_anthropic-0.1.0/tests/conftest.py +11 -0
- iris_security_anthropic-0.1.0/tests/test_anthropic_integration.py +262 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iris-security-anthropic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IRIS governance for Anthropic Claude — Cedar policy on every API call
|
|
5
|
+
Author-email: IRIS Platform <sdk@iris.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/gimartinb/iris-sdk
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: iris-security-core>=0.1.0
|
|
12
|
+
Requires-Dist: iris-security-sdk>=0.1.0
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: anthropic>=0.25; extra == "anthropic"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# iris-anthropic
|
|
21
|
+
|
|
22
|
+
Drop-in IRIS governance for the [Anthropic Python SDK](https://github.com/anthropics/anthropic-sdk-python).
|
|
23
|
+
|
|
24
|
+
Replace one line:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# client = anthropic.Anthropic()
|
|
28
|
+
client = IrisAnthropic(passport=passport)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every `client.messages.create()` and `client.messages.stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install iris-anthropic
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
See [examples/governed_claude.py](examples/governed_claude.py).
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| `IRIS_ENV` | Behavior |
|
|
46
|
+
|-------------|-----------------------------------------------|
|
|
47
|
+
| `dev` | Fail open — warnings to stderr, never block |
|
|
48
|
+
| `production`| Fail closed — `IrisViolationError` on deny |
|
|
49
|
+
|
|
50
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# iris-anthropic
|
|
2
|
+
|
|
3
|
+
Drop-in IRIS governance for the [Anthropic Python SDK](https://github.com/anthropics/anthropic-sdk-python).
|
|
4
|
+
|
|
5
|
+
Replace one line:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# client = anthropic.Anthropic()
|
|
9
|
+
client = IrisAnthropic(passport=passport)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Every `client.messages.create()` and `client.messages.stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install iris-anthropic
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
See [examples/governed_claude.py](examples/governed_claude.py).
|
|
23
|
+
|
|
24
|
+
## Environment
|
|
25
|
+
|
|
26
|
+
| `IRIS_ENV` | Behavior |
|
|
27
|
+
|-------------|-----------------------------------------------|
|
|
28
|
+
| `dev` | Fail open — warnings to stderr, never block |
|
|
29
|
+
| `production`| Fail closed — `IrisViolationError` on deny |
|
|
30
|
+
|
|
31
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal IRIS + Anthropic integration — one line changes from the stock SDK.
|
|
3
|
+
|
|
4
|
+
Replace:
|
|
5
|
+
client = anthropic.Anthropic()
|
|
6
|
+
With:
|
|
7
|
+
client = IrisAnthropic(passport=passport)
|
|
8
|
+
|
|
9
|
+
All other Anthropic usage stays the same.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from iris import AgentPassport, ComplianceTag
|
|
13
|
+
from iris_anthropic import IrisAnthropic
|
|
14
|
+
|
|
15
|
+
passport = AgentPassport(
|
|
16
|
+
name="support-agent",
|
|
17
|
+
owner="team@company.com",
|
|
18
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
19
|
+
is_high_risk_ai=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
client = IrisAnthropic(passport=passport)
|
|
23
|
+
|
|
24
|
+
message = client.messages.create(
|
|
25
|
+
model="claude-sonnet-4-6",
|
|
26
|
+
max_tokens=1024,
|
|
27
|
+
messages=[{"role": "user", "content": "Help this customer."}],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# IRIS evaluated this call. Policy enforced. Evidence logged.
|
|
31
|
+
print(message.content[0].text)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IRIS Anthropic integration — one-line drop-in for anthropic.Anthropic().
|
|
3
|
+
|
|
4
|
+
Quickstart:
|
|
5
|
+
from iris_anthropic import IrisAnthropic
|
|
6
|
+
from iris import AgentPassport, ComplianceTag
|
|
7
|
+
|
|
8
|
+
passport = AgentPassport(
|
|
9
|
+
name="support-agent",
|
|
10
|
+
owner="team@company.com",
|
|
11
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
12
|
+
)
|
|
13
|
+
client = IrisAnthropic(passport=passport)
|
|
14
|
+
message = client.messages.create(model="claude-sonnet-4-6", ...)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from iris import IrisViolationError
|
|
20
|
+
from iris_core.models.passport import (
|
|
21
|
+
AgentPassport,
|
|
22
|
+
ComplianceTag,
|
|
23
|
+
DataClassification,
|
|
24
|
+
Environment,
|
|
25
|
+
)
|
|
26
|
+
from iris_core.models.policy import Violation
|
|
27
|
+
|
|
28
|
+
from iris_anthropic.client import IrisAnthropic, IrisAnthropicAsync
|
|
29
|
+
from iris_anthropic.guardrails import check_prompt_for_violations
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"IrisAnthropic",
|
|
35
|
+
"IrisAnthropicAsync",
|
|
36
|
+
"IrisViolationError",
|
|
37
|
+
"AgentPassport",
|
|
38
|
+
"ComplianceTag",
|
|
39
|
+
"DataClassification",
|
|
40
|
+
"Environment",
|
|
41
|
+
"Violation",
|
|
42
|
+
"check_prompt_for_violations",
|
|
43
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Shared IRIS evaluation helpers for Anthropic SDK integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from iris import IrisViolationError
|
|
13
|
+
from iris_core.engine.cedar import CedarEngine, EvaluationContext
|
|
14
|
+
from iris_core.rbac.context import UserContext
|
|
15
|
+
from iris_core.evidence.vault import EvidenceVault
|
|
16
|
+
from iris_core.models.passport import AgentPassport, Environment
|
|
17
|
+
from iris_core.models.policy import PolicyResult, Violation
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("iris.anthropic")
|
|
20
|
+
|
|
21
|
+
_VAULT_LOCK = threading.Lock()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def current_environment() -> Environment:
|
|
25
|
+
return Environment(os.environ.get("IRIS_ENV", "dev"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def has_policy_loaded(engine: CedarEngine, passport: AgentPassport) -> bool:
|
|
29
|
+
return bool(engine._policy_cache.get(passport.agent_id))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_passport_policy(engine: CedarEngine, passport: AgentPassport) -> None:
|
|
33
|
+
if not passport.policy_ref:
|
|
34
|
+
return
|
|
35
|
+
policy_path = Path(passport.policy_ref)
|
|
36
|
+
if not policy_path.is_absolute():
|
|
37
|
+
policy_path = Path.cwd() / policy_path
|
|
38
|
+
if policy_path.exists():
|
|
39
|
+
engine.load_policy_file(passport.agent_id, policy_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_no_policy_gate(
|
|
43
|
+
engine: CedarEngine,
|
|
44
|
+
passport: AgentPassport,
|
|
45
|
+
env: Environment,
|
|
46
|
+
result: PolicyResult,
|
|
47
|
+
) -> PolicyResult:
|
|
48
|
+
"""Fail open in dev/test when no policy is loaded; fail closed in staging/prod."""
|
|
49
|
+
if has_policy_loaded(engine, passport):
|
|
50
|
+
return result
|
|
51
|
+
if env in (Environment.DEV, Environment.TEST):
|
|
52
|
+
if result.decision == "DENY":
|
|
53
|
+
return PolicyResult(
|
|
54
|
+
decision="PERMIT_WITH_WARNINGS",
|
|
55
|
+
violations=result.violations,
|
|
56
|
+
agent_id=result.agent_id,
|
|
57
|
+
action=result.action,
|
|
58
|
+
resource=result.resource,
|
|
59
|
+
environment=result.environment,
|
|
60
|
+
)
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def merge_prompt_violations(result: PolicyResult, prompt_violations: List[Violation]) -> PolicyResult:
|
|
65
|
+
if not prompt_violations:
|
|
66
|
+
return result
|
|
67
|
+
from iris_core.models.policy import Severity
|
|
68
|
+
|
|
69
|
+
violations = list(result.violations) + list(prompt_violations)
|
|
70
|
+
critical = [v for v in violations if v.severity == Severity.CRITICAL]
|
|
71
|
+
if critical:
|
|
72
|
+
decision = "DENY"
|
|
73
|
+
elif violations and result.decision == "PERMIT":
|
|
74
|
+
env = Environment(result.environment) if result.environment else Environment.DEV
|
|
75
|
+
if env in (Environment.DEV, Environment.TEST):
|
|
76
|
+
decision = "PERMIT_WITH_WARNINGS"
|
|
77
|
+
else:
|
|
78
|
+
high = [v for v in violations if v.severity in (Severity.HIGH, Severity.CRITICAL)]
|
|
79
|
+
decision = "DENY" if high else "PERMIT_WITH_WARNINGS"
|
|
80
|
+
else:
|
|
81
|
+
decision = result.decision
|
|
82
|
+
return PolicyResult(
|
|
83
|
+
decision=decision,
|
|
84
|
+
violations=violations,
|
|
85
|
+
agent_id=result.agent_id,
|
|
86
|
+
action=result.action,
|
|
87
|
+
resource=result.resource,
|
|
88
|
+
environment=result.environment,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def evaluate_api_call(
|
|
93
|
+
engine: CedarEngine,
|
|
94
|
+
vault: EvidenceVault,
|
|
95
|
+
passport: AgentPassport,
|
|
96
|
+
env: Environment,
|
|
97
|
+
*,
|
|
98
|
+
data_classification: Optional[str] = None,
|
|
99
|
+
prompt_violations: Optional[List[Violation]] = None,
|
|
100
|
+
additional: Optional[dict] = None,
|
|
101
|
+
dlp_prompt_findings: Optional[list] = None,
|
|
102
|
+
user_email: Optional[str] = None,
|
|
103
|
+
user_role: Optional[str] = None,
|
|
104
|
+
) -> PolicyResult:
|
|
105
|
+
user_ctx = UserContext.from_params(user_email, user_role)
|
|
106
|
+
ctx = EvaluationContext(
|
|
107
|
+
agent_id=passport.agent_id,
|
|
108
|
+
action="call",
|
|
109
|
+
resource="anthropic-api",
|
|
110
|
+
resource_type="api",
|
|
111
|
+
environment=env,
|
|
112
|
+
data_classification=data_classification or passport.data_classification.value,
|
|
113
|
+
dlp_prompt_findings=dlp_prompt_findings,
|
|
114
|
+
additional=additional or {},
|
|
115
|
+
**user_ctx.evaluation_fields(),
|
|
116
|
+
)
|
|
117
|
+
result = engine.evaluate(passport, ctx)
|
|
118
|
+
result = apply_no_policy_gate(engine, passport, env, result)
|
|
119
|
+
result = merge_prompt_violations(result, prompt_violations or [])
|
|
120
|
+
with _VAULT_LOCK:
|
|
121
|
+
vault.record(ctx, result)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def enforce_result(result: PolicyResult, env: Environment) -> None:
|
|
126
|
+
if result.decision == "DENY":
|
|
127
|
+
if env in (Environment.DEV, Environment.TEST):
|
|
128
|
+
for violation in result.violations:
|
|
129
|
+
msg = (
|
|
130
|
+
f"[IRIS WARNING] {violation.message} "
|
|
131
|
+
f"Remediation: {violation.remediation}"
|
|
132
|
+
)
|
|
133
|
+
logger.warning(msg)
|
|
134
|
+
print(msg, file=sys.stderr)
|
|
135
|
+
return
|
|
136
|
+
raise IrisViolationError(result)
|
|
137
|
+
if result.decision == "PERMIT_WITH_WARNINGS":
|
|
138
|
+
for violation in result.violations:
|
|
139
|
+
msg = (
|
|
140
|
+
f"[IRIS WARNING] {violation.message} "
|
|
141
|
+
f"Remediation: {violation.remediation}"
|
|
142
|
+
)
|
|
143
|
+
logger.warning(msg)
|
|
144
|
+
print(msg, file=sys.stderr)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Drop-in Anthropic client wrapper with IRIS governance on every messages call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from iris_core.dlp import DLPScanner
|
|
8
|
+
from iris_core.dlp.enforcement import (
|
|
9
|
+
enforce_prompt_dlp,
|
|
10
|
+
extract_anthropic_response_text,
|
|
11
|
+
handle_response_dlp,
|
|
12
|
+
)
|
|
13
|
+
from iris_core.engine.cedar import CedarEngine
|
|
14
|
+
from iris_core.evidence.vault import EvidenceVault
|
|
15
|
+
from iris_core.models.passport import AgentPassport
|
|
16
|
+
from iris_anthropic._governance import (
|
|
17
|
+
current_environment,
|
|
18
|
+
enforce_result,
|
|
19
|
+
evaluate_api_call,
|
|
20
|
+
load_passport_policy,
|
|
21
|
+
)
|
|
22
|
+
from iris_anthropic.guardrails import (
|
|
23
|
+
check_prompt_for_violations,
|
|
24
|
+
effective_data_classification,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _lazy_anthropic():
|
|
29
|
+
import anthropic
|
|
30
|
+
|
|
31
|
+
return anthropic
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_prompt_text(kwargs: dict) -> str:
|
|
35
|
+
parts: List[str] = []
|
|
36
|
+
system = kwargs.get("system")
|
|
37
|
+
if system:
|
|
38
|
+
if isinstance(system, str):
|
|
39
|
+
parts.append(system)
|
|
40
|
+
elif isinstance(system, list):
|
|
41
|
+
for block in system:
|
|
42
|
+
if isinstance(block, dict):
|
|
43
|
+
text = block.get("text") or block.get("content")
|
|
44
|
+
if text:
|
|
45
|
+
parts.append(str(text))
|
|
46
|
+
for msg in kwargs.get("messages") or []:
|
|
47
|
+
if not isinstance(msg, dict):
|
|
48
|
+
continue
|
|
49
|
+
content = msg.get("content")
|
|
50
|
+
if isinstance(content, str):
|
|
51
|
+
parts.append(content)
|
|
52
|
+
elif isinstance(content, list):
|
|
53
|
+
for block in content:
|
|
54
|
+
if isinstance(block, dict):
|
|
55
|
+
text = block.get("text") or block.get("content")
|
|
56
|
+
if text:
|
|
57
|
+
parts.append(str(text))
|
|
58
|
+
return "\n".join(parts)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _IrisAnthropicClientBase:
|
|
62
|
+
_passport: AgentPassport
|
|
63
|
+
_engine: CedarEngine
|
|
64
|
+
_vault: EvidenceVault
|
|
65
|
+
_dlp: DLPScanner
|
|
66
|
+
_user_email: Optional[str] = None
|
|
67
|
+
_user_role: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _GovernedMessagesBase:
|
|
71
|
+
"""Uses the parent client so engine/vault stay in sync when replaced in tests."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, parent: _IrisAnthropicClientBase, messages_resource: Any):
|
|
74
|
+
self._parent = parent
|
|
75
|
+
self._messages = messages_resource
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def _passport(self) -> AgentPassport:
|
|
79
|
+
return self._parent._passport
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def _engine(self) -> CedarEngine:
|
|
83
|
+
return self._parent._engine
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def _vault(self) -> EvidenceVault:
|
|
87
|
+
return self._parent._vault
|
|
88
|
+
|
|
89
|
+
def _govern_kwargs(self, kwargs: dict) -> None:
|
|
90
|
+
env = current_environment()
|
|
91
|
+
prompt = _extract_prompt_text(kwargs)
|
|
92
|
+
dlp_result = enforce_prompt_dlp(
|
|
93
|
+
self._parent._dlp,
|
|
94
|
+
self._vault,
|
|
95
|
+
self._passport,
|
|
96
|
+
env,
|
|
97
|
+
prompt,
|
|
98
|
+
resource="anthropic-api",
|
|
99
|
+
)
|
|
100
|
+
prompt_violations = check_prompt_for_violations(prompt, self._passport)
|
|
101
|
+
data_classification = effective_data_classification(prompt, self._passport)
|
|
102
|
+
additional = {
|
|
103
|
+
"model": kwargs.get("model"),
|
|
104
|
+
"max_tokens": kwargs.get("max_tokens"),
|
|
105
|
+
"prompt_violation_count": len(prompt_violations),
|
|
106
|
+
}
|
|
107
|
+
if prompt_violations:
|
|
108
|
+
additional["prompt_violations"] = [v.rule_id for v in prompt_violations]
|
|
109
|
+
|
|
110
|
+
result = evaluate_api_call(
|
|
111
|
+
self._engine,
|
|
112
|
+
self._vault,
|
|
113
|
+
self._passport,
|
|
114
|
+
env,
|
|
115
|
+
data_classification=data_classification,
|
|
116
|
+
prompt_violations=prompt_violations,
|
|
117
|
+
additional=additional,
|
|
118
|
+
dlp_prompt_findings=dlp_result.findings,
|
|
119
|
+
user_email=self._parent._user_email,
|
|
120
|
+
user_role=self._parent._user_role,
|
|
121
|
+
)
|
|
122
|
+
enforce_result(result, env)
|
|
123
|
+
|
|
124
|
+
def _scan_response(self, response: Any) -> Any:
|
|
125
|
+
env = current_environment()
|
|
126
|
+
response_text = extract_anthropic_response_text(response)
|
|
127
|
+
blocked, _ = handle_response_dlp(
|
|
128
|
+
self._parent._dlp,
|
|
129
|
+
self._vault,
|
|
130
|
+
self._passport,
|
|
131
|
+
env,
|
|
132
|
+
response_text,
|
|
133
|
+
response,
|
|
134
|
+
resource="anthropic-api",
|
|
135
|
+
)
|
|
136
|
+
return blocked
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IrisMessagesResource(_GovernedMessagesBase):
|
|
140
|
+
def create(self, **kwargs: Any) -> Any:
|
|
141
|
+
self._govern_kwargs(kwargs)
|
|
142
|
+
response = self._messages.create(**kwargs)
|
|
143
|
+
return self._scan_response(response)
|
|
144
|
+
|
|
145
|
+
def stream(self, **kwargs: Any) -> Any:
|
|
146
|
+
self._govern_kwargs(kwargs)
|
|
147
|
+
return self._messages.stream(**kwargs)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class IrisMessagesResourceAsync(_GovernedMessagesBase):
|
|
151
|
+
async def create(self, **kwargs: Any) -> Any:
|
|
152
|
+
self._govern_kwargs(kwargs)
|
|
153
|
+
response = await self._messages.create(**kwargs)
|
|
154
|
+
return self._scan_response(response)
|
|
155
|
+
|
|
156
|
+
async def stream(self, **kwargs: Any) -> Any:
|
|
157
|
+
self._govern_kwargs(kwargs)
|
|
158
|
+
return await self._messages.stream(**kwargs)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class IrisAnthropic(_IrisAnthropicClientBase):
|
|
162
|
+
"""
|
|
163
|
+
Drop-in replacement for anthropic.Anthropic() with IRIS governance.
|
|
164
|
+
|
|
165
|
+
Pass an AgentPassport and the same kwargs you would give Anthropic().
|
|
166
|
+
All attributes not defined here are proxied to the underlying client.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
passport: AgentPassport,
|
|
172
|
+
user_email: Optional[str] = None,
|
|
173
|
+
user_role: Optional[str] = None,
|
|
174
|
+
**anthropic_kwargs: Any,
|
|
175
|
+
):
|
|
176
|
+
from iris_core.dev_trust import print_dev_trust_message
|
|
177
|
+
|
|
178
|
+
print_dev_trust_message()
|
|
179
|
+
anthropic = _lazy_anthropic()
|
|
180
|
+
self._passport = passport
|
|
181
|
+
self._user_email = user_email
|
|
182
|
+
self._user_role = user_role
|
|
183
|
+
self._engine = CedarEngine()
|
|
184
|
+
self._vault = EvidenceVault(agent_id=passport.agent_id)
|
|
185
|
+
self._dlp = DLPScanner(passport)
|
|
186
|
+
load_passport_policy(self._engine, passport)
|
|
187
|
+
self._client = anthropic.Anthropic(**anthropic_kwargs)
|
|
188
|
+
self._messages_resource = IrisMessagesResource(self, self._client.messages)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def messages(self) -> IrisMessagesResource:
|
|
192
|
+
return self._messages_resource
|
|
193
|
+
|
|
194
|
+
def __getattr__(self, name: str) -> Any:
|
|
195
|
+
return getattr(self._client, name)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class IrisAnthropicAsync(_IrisAnthropicClientBase):
|
|
199
|
+
"""Async drop-in replacement for anthropic.AsyncAnthropic()."""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
passport: AgentPassport,
|
|
204
|
+
user_email: Optional[str] = None,
|
|
205
|
+
user_role: Optional[str] = None,
|
|
206
|
+
**anthropic_kwargs: Any,
|
|
207
|
+
):
|
|
208
|
+
anthropic = _lazy_anthropic()
|
|
209
|
+
self._passport = passport
|
|
210
|
+
self._user_email = user_email
|
|
211
|
+
self._user_role = user_role
|
|
212
|
+
self._engine = CedarEngine()
|
|
213
|
+
self._vault = EvidenceVault(agent_id=passport.agent_id)
|
|
214
|
+
self._dlp = DLPScanner(passport)
|
|
215
|
+
load_passport_policy(self._engine, passport)
|
|
216
|
+
self._client = anthropic.AsyncAnthropic(**anthropic_kwargs)
|
|
217
|
+
self._messages_resource = IrisMessagesResourceAsync(self, self._client.messages)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def messages(self) -> IrisMessagesResourceAsync:
|
|
221
|
+
return self._messages_resource
|
|
222
|
+
|
|
223
|
+
def __getattr__(self, name: str) -> Any:
|
|
224
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Prompt guardrails for Anthropic API calls — scan-only, non-blocking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from iris_core.models.passport import AgentPassport, DataClassification
|
|
9
|
+
from iris_core.models.policy import Severity, Violation
|
|
10
|
+
|
|
11
|
+
_SSN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
|
|
12
|
+
_CREDIT_CARD = re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b")
|
|
13
|
+
_DOB = re.compile(
|
|
14
|
+
r"\b(?:0[1-9]|1[0-2])[/-](?:0[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2}\b"
|
|
15
|
+
)
|
|
16
|
+
_CROSS_REGION = re.compile(r"cn-north|china|beijing", re.IGNORECASE)
|
|
17
|
+
_HIGH_RISK_DOMAIN = re.compile(r"\b(loan|diagnosis|hiring)\b", re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_prompt_for_violations(prompt: str, passport: AgentPassport) -> List[Violation]:
|
|
21
|
+
"""
|
|
22
|
+
Scan prompt text for policy-relevant patterns.
|
|
23
|
+
|
|
24
|
+
Returns violations without blocking — the caller merges them into evaluation.
|
|
25
|
+
"""
|
|
26
|
+
if not prompt:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
violations: List[Violation] = []
|
|
30
|
+
|
|
31
|
+
if _SSN.search(prompt) or _CREDIT_CARD.search(prompt) or _DOB.search(prompt):
|
|
32
|
+
violations.append(
|
|
33
|
+
Violation(
|
|
34
|
+
rule_id="IRIS-DATA-001",
|
|
35
|
+
severity=Severity.HIGH,
|
|
36
|
+
message=(
|
|
37
|
+
f"Prompt for agent '{passport.name}' may contain PII "
|
|
38
|
+
f"(SSN, payment card, or date-of-birth pattern). "
|
|
39
|
+
f"Passport data classification is '{passport.data_classification.value}'."
|
|
40
|
+
),
|
|
41
|
+
compliance_refs=[
|
|
42
|
+
"colorado-ai-act:impact-assessment",
|
|
43
|
+
"gdpr:data-minimization",
|
|
44
|
+
],
|
|
45
|
+
remediation=(
|
|
46
|
+
"Remove sensitive identifiers from the prompt or update the agent "
|
|
47
|
+
"passport data_classification to match the data being processed."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if _CROSS_REGION.search(prompt):
|
|
53
|
+
violations.append(
|
|
54
|
+
Violation(
|
|
55
|
+
rule_id="IRIS-XR-001",
|
|
56
|
+
severity=Severity.CRITICAL,
|
|
57
|
+
message=(
|
|
58
|
+
f"Prompt for agent '{passport.name}' references restricted "
|
|
59
|
+
f"cross-region geography (China / cn-north)."
|
|
60
|
+
),
|
|
61
|
+
compliance_refs=["china-pipl:cross-border-transfer"],
|
|
62
|
+
remediation=(
|
|
63
|
+
"Remove cross-region references from the prompt or document an "
|
|
64
|
+
"approved exception with your security engineer."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if _HIGH_RISK_DOMAIN.search(prompt):
|
|
70
|
+
violations.append(
|
|
71
|
+
Violation(
|
|
72
|
+
rule_id="CO-004",
|
|
73
|
+
severity=Severity.HIGH,
|
|
74
|
+
message=(
|
|
75
|
+
f"Prompt for agent '{passport.name}' references a high-risk "
|
|
76
|
+
f"consequential domain (loan, diagnosis, or hiring). "
|
|
77
|
+
f"Consumer opt-out and consent may be required under SB 24-205."
|
|
78
|
+
),
|
|
79
|
+
compliance_refs=["colorado-ai-act:sb-24-205:consumer-opt-out"],
|
|
80
|
+
remediation=(
|
|
81
|
+
"Set user_consent_logged=True in policy context for consequential "
|
|
82
|
+
"actions, or run 'iris compliance assess --agent "
|
|
83
|
+
f"{passport.agent_id} --framework colorado-ai-act'."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return violations
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def prompt_suggests_pii(prompt: str) -> bool:
|
|
92
|
+
"""Whether guardrails detected PII patterns (used to elevate data_classification)."""
|
|
93
|
+
if not prompt:
|
|
94
|
+
return False
|
|
95
|
+
return bool(_SSN.search(prompt) or _CREDIT_CARD.search(prompt) or _DOB.search(prompt))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def effective_data_classification(prompt: str, passport: AgentPassport) -> str:
|
|
99
|
+
if prompt_suggests_pii(prompt):
|
|
100
|
+
return DataClassification.PII.value
|
|
101
|
+
return passport.data_classification.value
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iris-security-anthropic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IRIS governance for Anthropic Claude — Cedar policy on every API call
|
|
5
|
+
Author-email: IRIS Platform <sdk@iris.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/gimartinb/iris-sdk
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: iris-security-core>=0.1.0
|
|
12
|
+
Requires-Dist: iris-security-sdk>=0.1.0
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: anthropic>=0.25; extra == "anthropic"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# iris-anthropic
|
|
21
|
+
|
|
22
|
+
Drop-in IRIS governance for the [Anthropic Python SDK](https://github.com/anthropics/anthropic-sdk-python).
|
|
23
|
+
|
|
24
|
+
Replace one line:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# client = anthropic.Anthropic()
|
|
28
|
+
client = IrisAnthropic(passport=passport)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every `client.messages.create()` and `client.messages.stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install iris-anthropic
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
See [examples/governed_claude.py](examples/governed_claude.py).
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| `IRIS_ENV` | Behavior |
|
|
46
|
+
|-------------|-----------------------------------------------|
|
|
47
|
+
| `dev` | Fail open — warnings to stderr, never block |
|
|
48
|
+
| `production`| Fail closed — `IrisViolationError` on deny |
|
|
49
|
+
|
|
50
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
examples/governed_claude.py
|
|
4
|
+
iris_anthropic/__init__.py
|
|
5
|
+
iris_anthropic/_governance.py
|
|
6
|
+
iris_anthropic/client.py
|
|
7
|
+
iris_anthropic/guardrails.py
|
|
8
|
+
iris_security_anthropic.egg-info/PKG-INFO
|
|
9
|
+
iris_security_anthropic.egg-info/SOURCES.txt
|
|
10
|
+
iris_security_anthropic.egg-info/dependency_links.txt
|
|
11
|
+
iris_security_anthropic.egg-info/requires.txt
|
|
12
|
+
iris_security_anthropic.egg-info/top_level.txt
|
|
13
|
+
tests/conftest.py
|
|
14
|
+
tests/test_anthropic_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iris-security-anthropic"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "IRIS governance for Anthropic Claude — Cedar policy on every API call"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "IRIS Platform", email = "sdk@iris.ai" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"iris-security-core>=0.1.0",
|
|
15
|
+
"iris-security-sdk>=0.1.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
anthropic = ["anthropic>=0.25"]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"pytest-asyncio>=0.23",
|
|
23
|
+
"ruff>=0.4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/gimartinb/iris-sdk"
|
|
28
|
+
Repository = "https://github.com/gimartinb/iris-sdk"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Test fixtures — isolate Evidence Vault from the developer home directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def iris_home_tmp(monkeypatch, tmp_path):
|
|
10
|
+
"""Write ~/.iris evidence under pytest tmp_path."""
|
|
11
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for iris-anthropic — mocked Anthropic API, no network calls.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from iris import IrisViolationError
|
|
12
|
+
from iris_core.engine.cedar import CedarEngine
|
|
13
|
+
from iris_core.evidence.vault import EvidenceVault
|
|
14
|
+
from iris_core.models.passport import (
|
|
15
|
+
AgentPassport,
|
|
16
|
+
ComplianceTag,
|
|
17
|
+
DataClassification,
|
|
18
|
+
Environment,
|
|
19
|
+
)
|
|
20
|
+
from iris_anthropic import IrisAnthropic, check_prompt_for_violations
|
|
21
|
+
from iris_anthropic.guardrails import prompt_suggests_pii
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def compliant_passport():
|
|
26
|
+
return AgentPassport(
|
|
27
|
+
name="support-agent",
|
|
28
|
+
owner="team@company.com",
|
|
29
|
+
data_classification=DataClassification.INTERNAL,
|
|
30
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
31
|
+
environments=[Environment.DEV, Environment.PRODUCTION],
|
|
32
|
+
is_high_risk_ai=True,
|
|
33
|
+
evidence_vault_id="vault-abc",
|
|
34
|
+
intent_ref="governance/agents/support-agent/policy-intent.md",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def high_risk_incomplete_passport():
|
|
40
|
+
return AgentPassport(
|
|
41
|
+
name="loan-agent",
|
|
42
|
+
owner="gmoney@gmail.com",
|
|
43
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
44
|
+
environments=[Environment.DEV, Environment.PRODUCTION],
|
|
45
|
+
is_high_risk_ai=True,
|
|
46
|
+
evidence_vault_id=None,
|
|
47
|
+
intent_ref=None,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _mock_anthropic_module():
|
|
52
|
+
mock_module = MagicMock()
|
|
53
|
+
mock_response = MagicMock()
|
|
54
|
+
mock_response.content = [MagicMock(text="Hello from Claude")]
|
|
55
|
+
mock_client = MagicMock()
|
|
56
|
+
mock_client.messages.create.return_value = mock_response
|
|
57
|
+
mock_client.messages.stream.return_value = iter([mock_response])
|
|
58
|
+
mock_module.Anthropic.return_value = mock_client
|
|
59
|
+
return mock_module, mock_client
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestIrisAnthropicClient:
|
|
63
|
+
def test_client_permits_allowed_call(self, compliant_passport, tmp_path, monkeypatch):
|
|
64
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
65
|
+
mock_module, mock_client = _mock_anthropic_module()
|
|
66
|
+
engine = CedarEngine()
|
|
67
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
68
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
69
|
+
|
|
70
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
71
|
+
client = IrisAnthropic(passport=compliant_passport)
|
|
72
|
+
client._engine = engine
|
|
73
|
+
client._vault = vault
|
|
74
|
+
|
|
75
|
+
result = client.messages.create(
|
|
76
|
+
model="claude-sonnet-4-6",
|
|
77
|
+
max_tokens=1024,
|
|
78
|
+
messages=[{"role": "user", "content": "Help this customer."}],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert result.content[0].text == "Hello from Claude"
|
|
82
|
+
mock_client.messages.create.assert_called_once()
|
|
83
|
+
events = vault.get_events()
|
|
84
|
+
assert len(events) == 1
|
|
85
|
+
assert events[0]["decision"] in ("PERMIT", "PERMIT_WITH_WARNINGS")
|
|
86
|
+
|
|
87
|
+
def test_client_blocks_denied_call_in_production(
|
|
88
|
+
self, high_risk_incomplete_passport, tmp_path, monkeypatch
|
|
89
|
+
):
|
|
90
|
+
monkeypatch.setenv("IRIS_ENV", "production")
|
|
91
|
+
mock_module, _ = _mock_anthropic_module()
|
|
92
|
+
engine = CedarEngine()
|
|
93
|
+
engine.load_policy(
|
|
94
|
+
high_risk_incomplete_passport.agent_id,
|
|
95
|
+
"permit(principal, action, resource);",
|
|
96
|
+
)
|
|
97
|
+
vault = EvidenceVault(
|
|
98
|
+
agent_id=high_risk_incomplete_passport.agent_id, vault_dir=tmp_path
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
102
|
+
client = IrisAnthropic(passport=high_risk_incomplete_passport)
|
|
103
|
+
client._engine = engine
|
|
104
|
+
client._vault = vault
|
|
105
|
+
|
|
106
|
+
with pytest.raises(IrisViolationError) as exc_info:
|
|
107
|
+
client.messages.create(
|
|
108
|
+
model="claude-sonnet-4-6",
|
|
109
|
+
max_tokens=1024,
|
|
110
|
+
messages=[{"role": "user", "content": "Help this customer."}],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert exc_info.value.result.decision == "DENY"
|
|
114
|
+
assert any(v.rule_id == "CO-001" for v in exc_info.value.result.violations)
|
|
115
|
+
mock_module.Anthropic.return_value.messages.create.assert_not_called()
|
|
116
|
+
|
|
117
|
+
def test_client_warns_in_dev_environment(
|
|
118
|
+
self, high_risk_incomplete_passport, tmp_path, monkeypatch, capsys
|
|
119
|
+
):
|
|
120
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
121
|
+
mock_module, mock_client = _mock_anthropic_module()
|
|
122
|
+
engine = CedarEngine()
|
|
123
|
+
engine.load_policy(
|
|
124
|
+
high_risk_incomplete_passport.agent_id,
|
|
125
|
+
"permit(principal, action, resource);",
|
|
126
|
+
)
|
|
127
|
+
vault = EvidenceVault(
|
|
128
|
+
agent_id=high_risk_incomplete_passport.agent_id, vault_dir=tmp_path
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
132
|
+
client = IrisAnthropic(passport=high_risk_incomplete_passport)
|
|
133
|
+
client._engine = engine
|
|
134
|
+
client._vault = vault
|
|
135
|
+
|
|
136
|
+
client.messages.create(
|
|
137
|
+
model="claude-sonnet-4-6",
|
|
138
|
+
max_tokens=1024,
|
|
139
|
+
messages=[{"role": "user", "content": "Help this customer."}],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
mock_client.messages.create.assert_called_once()
|
|
143
|
+
captured = capsys.readouterr()
|
|
144
|
+
assert "[IRIS WARNING]" in captured.err
|
|
145
|
+
events = vault.get_events()
|
|
146
|
+
assert events[0]["decision"] == "DENY"
|
|
147
|
+
|
|
148
|
+
def test_streaming_intercept(self, compliant_passport, tmp_path, monkeypatch):
|
|
149
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
150
|
+
mock_module, mock_client = _mock_anthropic_module()
|
|
151
|
+
engine = CedarEngine()
|
|
152
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
153
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
154
|
+
|
|
155
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
156
|
+
client = IrisAnthropic(passport=compliant_passport)
|
|
157
|
+
client._engine = engine
|
|
158
|
+
client._vault = vault
|
|
159
|
+
|
|
160
|
+
stream = client.messages.stream(
|
|
161
|
+
model="claude-sonnet-4-6",
|
|
162
|
+
max_tokens=256,
|
|
163
|
+
messages=[{"role": "user", "content": "Stream a short reply."}],
|
|
164
|
+
)
|
|
165
|
+
list(stream)
|
|
166
|
+
|
|
167
|
+
mock_client.messages.stream.assert_called_once()
|
|
168
|
+
assert len(vault.get_events()) == 1
|
|
169
|
+
|
|
170
|
+
def test_evidence_vault_records_every_call(self, compliant_passport, tmp_path, monkeypatch):
|
|
171
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
172
|
+
mock_module, _ = _mock_anthropic_module()
|
|
173
|
+
engine = CedarEngine()
|
|
174
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
175
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
176
|
+
|
|
177
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
178
|
+
client = IrisAnthropic(passport=compliant_passport)
|
|
179
|
+
client._engine = engine
|
|
180
|
+
client._vault = vault
|
|
181
|
+
|
|
182
|
+
client.messages.create(
|
|
183
|
+
model="claude-sonnet-4-6",
|
|
184
|
+
max_tokens=100,
|
|
185
|
+
messages=[{"role": "user", "content": "First call"}],
|
|
186
|
+
)
|
|
187
|
+
client.messages.create(
|
|
188
|
+
model="claude-sonnet-4-6",
|
|
189
|
+
max_tokens=100,
|
|
190
|
+
messages=[{"role": "user", "content": "Second call"}],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
events = vault.get_events()
|
|
194
|
+
assert len(events) == 2
|
|
195
|
+
assert all(e["action"] == "call" for e in events)
|
|
196
|
+
assert all(e["resource"] == "anthropic-api" for e in events)
|
|
197
|
+
|
|
198
|
+
def test_drop_in_replacement_api_compatibility(self, compliant_passport, monkeypatch):
|
|
199
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
200
|
+
mock_module, mock_client = _mock_anthropic_module()
|
|
201
|
+
mock_client.api_key = "sk-test-key"
|
|
202
|
+
mock_client.base_url = "https://api.anthropic.com"
|
|
203
|
+
|
|
204
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
205
|
+
client = IrisAnthropic(passport=compliant_passport, api_key="sk-test-key")
|
|
206
|
+
|
|
207
|
+
assert client.api_key == "sk-test-key"
|
|
208
|
+
assert client.base_url == "https://api.anthropic.com"
|
|
209
|
+
mock_module.Anthropic.assert_called_once_with(api_key="sk-test-key")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestPromptGuardrails:
|
|
213
|
+
def test_pii_pattern_detection_in_prompt(self, compliant_passport):
|
|
214
|
+
violations = check_prompt_for_violations(
|
|
215
|
+
"Customer SSN is 123-45-6789 for verification.",
|
|
216
|
+
compliant_passport,
|
|
217
|
+
)
|
|
218
|
+
assert any(v.rule_id == "IRIS-DATA-001" for v in violations)
|
|
219
|
+
assert prompt_suggests_pii("123-45-6789")
|
|
220
|
+
|
|
221
|
+
def test_cross_region_pattern_in_prompt(self, compliant_passport):
|
|
222
|
+
violations = check_prompt_for_violations(
|
|
223
|
+
"Deploy model to cn-north-1 region.",
|
|
224
|
+
compliant_passport,
|
|
225
|
+
)
|
|
226
|
+
assert any(v.rule_id == "IRIS-XR-001" for v in violations)
|
|
227
|
+
|
|
228
|
+
def test_high_risk_domain_keyword(self, compliant_passport):
|
|
229
|
+
violations = check_prompt_for_violations(
|
|
230
|
+
"Review this loan application for approval.",
|
|
231
|
+
compliant_passport,
|
|
232
|
+
)
|
|
233
|
+
assert any(v.rule_id == "CO-004" for v in violations)
|
|
234
|
+
|
|
235
|
+
def test_production_blocks_cross_region_prompt(
|
|
236
|
+
self, compliant_passport, tmp_path, monkeypatch
|
|
237
|
+
):
|
|
238
|
+
monkeypatch.setenv("IRIS_ENV", "production")
|
|
239
|
+
mock_module, mock_client = _mock_anthropic_module()
|
|
240
|
+
engine = CedarEngine()
|
|
241
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
242
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
243
|
+
|
|
244
|
+
with patch.dict("sys.modules", {"anthropic": mock_module}):
|
|
245
|
+
client = IrisAnthropic(passport=compliant_passport)
|
|
246
|
+
client._engine = engine
|
|
247
|
+
client._vault = vault
|
|
248
|
+
|
|
249
|
+
with pytest.raises(IrisViolationError) as exc_info:
|
|
250
|
+
client.messages.create(
|
|
251
|
+
model="claude-sonnet-4-6",
|
|
252
|
+
max_tokens=100,
|
|
253
|
+
messages=[
|
|
254
|
+
{
|
|
255
|
+
"role": "user",
|
|
256
|
+
"content": "Send data to beijing data center.",
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
assert any(v.rule_id == "IRIS-XR-001" for v in exc_info.value.result.violations)
|
|
262
|
+
mock_client.messages.create.assert_not_called()
|