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.
@@ -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,10 @@
1
+ iris-security-core>=0.1.0
2
+ iris-security-sdk>=0.1.0
3
+
4
+ [anthropic]
5
+ anthropic>=0.25
6
+
7
+ [dev]
8
+ pytest>=8.0
9
+ pytest-asyncio>=0.23
10
+ ruff>=0.4
@@ -0,0 +1,4 @@
1
+ dist
2
+ examples
3
+ iris_anthropic
4
+ tests
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()