iris-security-gemini 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-gemini
3
+ Version: 0.1.0
4
+ Summary: IRIS governance for Google Gemini via google-genai
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: google
14
+ Requires-Dist: google-genai>=0.3; extra == "google"
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-gemini
21
+
22
+ Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
23
+
24
+ Replace one line:
25
+
26
+ ```python
27
+ # client = google.genai.Client()
28
+ client = IrisGemini(passport=passport)
29
+ ```
30
+
31
+ Every `client.models.generate_content()` and `generate_content_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-security-gemini
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ See [examples/governed_gemini.py](examples/governed_gemini.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-gemini
2
+
3
+ Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
4
+
5
+ Replace one line:
6
+
7
+ ```python
8
+ # client = google.genai.Client()
9
+ client = IrisGemini(passport=passport)
10
+ ```
11
+
12
+ Every `client.models.generate_content()` and `generate_content_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-security-gemini
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ See [examples/governed_gemini.py](examples/governed_gemini.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
+ IRIS Gemini integration - one-line drop-in for google.genai.Client().
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from iris import IrisViolationError
8
+ from iris_core.models.passport import (
9
+ AgentPassport,
10
+ ComplianceTag,
11
+ DataClassification,
12
+ Environment,
13
+ )
14
+ from iris_core.models.policy import Violation
15
+
16
+ from iris_gemini.client import IrisGemini, IrisGeminiAsync
17
+ from iris_gemini.guardrails import scan_gemini_content
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "IrisGemini",
23
+ "IrisGeminiAsync",
24
+ "IrisViolationError",
25
+ "AgentPassport",
26
+ "ComplianceTag",
27
+ "DataClassification",
28
+ "Environment",
29
+ "Violation",
30
+ "scan_gemini_content",
31
+ ]
@@ -0,0 +1,304 @@
1
+ """Drop-in Google GenAI client wrapper with IRIS governance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import logging
7
+ import os
8
+ import sys
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ from iris import IrisViolationError
14
+ from iris_core.dlp import DLPScanner
15
+ from iris_core.dlp.enforcement import (
16
+ enforce_prompt_dlp,
17
+ extract_gemini_response_text,
18
+ handle_response_dlp,
19
+ )
20
+ from iris_core.engine.cedar import CedarEngine, EvaluationContext
21
+ from iris_core.rbac.context import UserContext
22
+ from iris_core.evidence.vault import EvidenceVault
23
+ from iris_core.models.passport import AgentPassport, Environment
24
+ from iris_core.models.policy import PolicyResult, Severity
25
+
26
+ from iris_gemini.guardrails import _extract_text, scan_gemini_content
27
+
28
+ logger = logging.getLogger("iris.gemini")
29
+ _VAULT_LOCK = threading.Lock()
30
+
31
+
32
+ def _lazy_genai():
33
+ try:
34
+ return importlib.import_module("google.genai")
35
+ except ModuleNotFoundError as exc:
36
+ raise ImportError(
37
+ "google-genai is required for IrisGemini. Install with: pip install google-genai"
38
+ ) from exc
39
+
40
+
41
+ def _current_environment() -> Environment:
42
+ return Environment(os.environ.get("IRIS_ENV", "dev"))
43
+
44
+
45
+ def _load_passport_policy(engine: CedarEngine, passport: AgentPassport) -> None:
46
+ if not passport.policy_ref:
47
+ return
48
+ policy_path = Path(passport.policy_ref)
49
+ if not policy_path.is_absolute():
50
+ policy_path = Path.cwd() / policy_path
51
+ if policy_path.exists():
52
+ engine.load_policy_file(passport.agent_id, policy_path)
53
+
54
+
55
+ def _has_policy_loaded(engine: CedarEngine, passport: AgentPassport) -> bool:
56
+ return bool(engine._policy_cache.get(passport.agent_id))
57
+
58
+
59
+ def _apply_no_policy_gate(
60
+ engine: CedarEngine,
61
+ passport: AgentPassport,
62
+ env: Environment,
63
+ result: PolicyResult,
64
+ ) -> PolicyResult:
65
+ if _has_policy_loaded(engine, passport):
66
+ return result
67
+ if env in (Environment.DEV, Environment.TEST) and result.decision == "DENY":
68
+ return PolicyResult(
69
+ decision="PERMIT_WITH_WARNINGS",
70
+ violations=result.violations,
71
+ agent_id=result.agent_id,
72
+ action=result.action,
73
+ resource=result.resource,
74
+ environment=result.environment,
75
+ )
76
+ return result
77
+
78
+
79
+ def _merge_content_violations(
80
+ result: PolicyResult, env: Environment, content_violations: list
81
+ ) -> PolicyResult:
82
+ if not content_violations:
83
+ return result
84
+ violations = list(result.violations) + list(content_violations)
85
+ critical = [v for v in violations if v.severity == Severity.CRITICAL]
86
+ high = [v for v in violations if v.severity in (Severity.HIGH, Severity.CRITICAL)]
87
+ if critical:
88
+ decision = "DENY"
89
+ elif high and result.decision == "PERMIT":
90
+ decision = "PERMIT_WITH_WARNINGS" if env in (Environment.DEV, Environment.TEST) else "DENY"
91
+ elif violations and result.decision == "PERMIT":
92
+ decision = "PERMIT_WITH_WARNINGS"
93
+ else:
94
+ decision = result.decision
95
+ return PolicyResult(
96
+ decision=decision,
97
+ violations=violations,
98
+ agent_id=result.agent_id,
99
+ action=result.action,
100
+ resource=result.resource,
101
+ environment=result.environment,
102
+ )
103
+
104
+
105
+ def _enforce_result(result: PolicyResult, env: Environment) -> None:
106
+ if result.decision == "DENY":
107
+ if env in (Environment.DEV, Environment.TEST):
108
+ for violation in result.violations:
109
+ msg = (
110
+ f"[IRIS WARNING] {violation.message} "
111
+ f"Remediation: {violation.remediation}"
112
+ )
113
+ logger.warning(msg)
114
+ print(msg, file=sys.stderr)
115
+ return
116
+ raise IrisViolationError(result)
117
+ if result.decision == "PERMIT_WITH_WARNINGS":
118
+ for violation in result.violations:
119
+ msg = f"[IRIS WARNING] {violation.message} Remediation: {violation.remediation}"
120
+ logger.warning(msg)
121
+ print(msg, file=sys.stderr)
122
+
123
+
124
+ class _IrisGeminiBase:
125
+ _passport: AgentPassport
126
+ _engine: CedarEngine
127
+ _vault: EvidenceVault
128
+ _dlp: DLPScanner
129
+ _user_email: Optional[str] = None
130
+ _user_role: Optional[str] = None
131
+
132
+
133
+ class IrisModelsResource:
134
+ """Governed wrapper around google.genai client.models."""
135
+
136
+ def __init__(self, parent: _IrisGeminiBase, models_resource: Any):
137
+ self._parent = parent
138
+ self._models = models_resource
139
+
140
+ @property
141
+ def _passport(self) -> AgentPassport:
142
+ return self._parent._passport
143
+
144
+ @property
145
+ def _engine(self) -> CedarEngine:
146
+ return self._parent._engine
147
+
148
+ @property
149
+ def _vault(self) -> EvidenceVault:
150
+ return self._parent._vault
151
+
152
+ def _govern(self, model: Optional[str], contents: Any, kwargs: dict) -> None:
153
+ env = _current_environment()
154
+ model_name = model or kwargs.get("model") or "unknown-model"
155
+ request_contents = contents if contents is not None else kwargs.get("contents")
156
+ prompt_text = "\n".join(_extract_text(request_contents))
157
+ dlp_result = enforce_prompt_dlp(
158
+ self._parent._dlp,
159
+ self._vault,
160
+ self._passport,
161
+ env,
162
+ prompt_text,
163
+ resource=f"gemini-api/{model_name}",
164
+ )
165
+ content_violations = scan_gemini_content(request_contents, self._passport)
166
+ user_ctx = UserContext.from_params(self._parent._user_email, self._parent._user_role)
167
+ ctx = EvaluationContext(
168
+ agent_id=self._passport.agent_id,
169
+ action="call",
170
+ resource=f"gemini-api/{model_name}",
171
+ resource_type="api",
172
+ environment=env,
173
+ data_classification=self._passport.data_classification.value,
174
+ dlp_prompt_findings=dlp_result.findings,
175
+ additional={
176
+ "model": model_name,
177
+ "content_violation_count": len(content_violations),
178
+ },
179
+ **user_ctx.evaluation_fields(),
180
+ )
181
+ result = self._engine.evaluate(self._passport, ctx)
182
+ result = _apply_no_policy_gate(self._engine, self._passport, env, result)
183
+ result = _merge_content_violations(result, env, content_violations)
184
+ with _VAULT_LOCK:
185
+ self._vault.record(ctx, result)
186
+ _enforce_result(result, env)
187
+
188
+ def _scan_response(self, response: Any) -> Any:
189
+ env = _current_environment()
190
+ response_text = extract_gemini_response_text(response)
191
+ blocked, _ = handle_response_dlp(
192
+ self._parent._dlp,
193
+ self._vault,
194
+ self._passport,
195
+ env,
196
+ response_text,
197
+ response,
198
+ resource="gemini-api",
199
+ )
200
+ return blocked
201
+
202
+ def generate_content(self, model: Any = None, contents: Any = None, **kwargs: Any) -> Any:
203
+ self._govern(model, contents, kwargs)
204
+ if model is not None:
205
+ kwargs["model"] = model
206
+ if contents is not None:
207
+ kwargs["contents"] = contents
208
+ response = self._models.generate_content(**kwargs)
209
+ return self._scan_response(response)
210
+
211
+ def generate_content_stream(
212
+ self, model: Any = None, contents: Any = None, **kwargs: Any
213
+ ) -> Any:
214
+ self._govern(model, contents, kwargs)
215
+ if model is not None:
216
+ kwargs["model"] = model
217
+ if contents is not None:
218
+ kwargs["contents"] = contents
219
+ return self._models.generate_content_stream(**kwargs)
220
+
221
+ async def generate_content_async(
222
+ self, model: Any = None, contents: Any = None, **kwargs: Any
223
+ ) -> Any:
224
+ self._govern(model, contents, kwargs)
225
+ if model is not None:
226
+ kwargs["model"] = model
227
+ if contents is not None:
228
+ kwargs["contents"] = contents
229
+ response = await self._models.generate_content_async(**kwargs)
230
+ return self._scan_response(response)
231
+
232
+ async def generate_content_stream_async(
233
+ self, model: Any = None, contents: Any = None, **kwargs: Any
234
+ ) -> Any:
235
+ self._govern(model, contents, kwargs)
236
+ if model is not None:
237
+ kwargs["model"] = model
238
+ if contents is not None:
239
+ kwargs["contents"] = contents
240
+ return await self._models.generate_content_stream_async(**kwargs)
241
+
242
+ def __getattr__(self, name: str) -> Any:
243
+ return getattr(self._models, name)
244
+
245
+
246
+ class IrisGemini(_IrisGeminiBase):
247
+ """Drop-in replacement for google.genai.Client()."""
248
+
249
+ def __init__(
250
+ self,
251
+ passport: AgentPassport,
252
+ user_email: Optional[str] = None,
253
+ user_role: Optional[str] = None,
254
+ **genai_kwargs: Any,
255
+ ):
256
+ from iris_core.dev_trust import print_dev_trust_message
257
+
258
+ print_dev_trust_message()
259
+ genai = _lazy_genai()
260
+ self._passport = passport
261
+ self._user_email = user_email
262
+ self._user_role = user_role
263
+ self._engine = CedarEngine()
264
+ self._vault = EvidenceVault(agent_id=passport.agent_id)
265
+ self._dlp = DLPScanner(passport)
266
+ _load_passport_policy(self._engine, passport)
267
+ self._client = genai.Client(**genai_kwargs)
268
+ self._models_resource = IrisModelsResource(self, self._client.models)
269
+
270
+ @property
271
+ def models(self) -> IrisModelsResource:
272
+ return self._models_resource
273
+
274
+ def __getattr__(self, name: str) -> Any:
275
+ return getattr(self._client, name)
276
+
277
+
278
+ class IrisGeminiAsync(_IrisGeminiBase):
279
+ """Async drop-in wrapper for google.genai.Client async methods."""
280
+
281
+ def __init__(
282
+ self,
283
+ passport: AgentPassport,
284
+ user_email: Optional[str] = None,
285
+ user_role: Optional[str] = None,
286
+ **genai_kwargs: Any,
287
+ ):
288
+ genai = _lazy_genai()
289
+ self._passport = passport
290
+ self._user_email = user_email
291
+ self._user_role = user_role
292
+ self._engine = CedarEngine()
293
+ self._vault = EvidenceVault(agent_id=passport.agent_id)
294
+ self._dlp = DLPScanner(passport)
295
+ _load_passport_policy(self._engine, passport)
296
+ self._client = genai.Client(**genai_kwargs)
297
+ self._models_resource = IrisModelsResource(self, self._client.models)
298
+
299
+ @property
300
+ def models(self) -> IrisModelsResource:
301
+ return self._models_resource
302
+
303
+ def __getattr__(self, name: str) -> Any:
304
+ return getattr(self._client, name)
@@ -0,0 +1,114 @@
1
+ """Guardrail scanning for Gemini contents payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, List
7
+
8
+ from iris_core.models.passport import AgentPassport
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 _extract_text(contents: Any) -> List[str]:
21
+ if contents is None:
22
+ return []
23
+ if isinstance(contents, str):
24
+ return [contents]
25
+ if isinstance(contents, dict):
26
+ values: List[str] = []
27
+ for key in ("text", "content"):
28
+ value = contents.get(key)
29
+ if isinstance(value, str):
30
+ values.append(value)
31
+ elif value is not None:
32
+ values.extend(_extract_text(value))
33
+ if not values:
34
+ for value in contents.values():
35
+ values.extend(_extract_text(value))
36
+ return values
37
+ if isinstance(contents, (list, tuple)):
38
+ values: List[str] = []
39
+ for item in contents:
40
+ values.extend(_extract_text(item))
41
+ return values
42
+
43
+ text = getattr(contents, "text", None)
44
+ if isinstance(text, str):
45
+ return [text]
46
+ content = getattr(contents, "content", None)
47
+ if content is not None:
48
+ return _extract_text(content)
49
+ return []
50
+
51
+
52
+ def scan_gemini_content(contents: Any, passport: AgentPassport) -> List[Violation]:
53
+ """Scan Gemini request contents and return policy violations."""
54
+ prompt = "\n".join(_extract_text(contents))
55
+ if not prompt:
56
+ return []
57
+
58
+ violations: List[Violation] = []
59
+
60
+ if _SSN.search(prompt) or _CREDIT_CARD.search(prompt) or _DOB.search(prompt):
61
+ violations.append(
62
+ Violation(
63
+ rule_id="IRIS-DATA-001",
64
+ severity=Severity.HIGH,
65
+ message=(
66
+ f"Gemini prompt for agent '{passport.name}' may contain PII "
67
+ "(SSN, payment card, or date-of-birth pattern)."
68
+ ),
69
+ compliance_refs=[
70
+ "colorado-ai-act:impact-assessment",
71
+ "gdpr:data-minimization",
72
+ ],
73
+ remediation=(
74
+ "Remove sensitive identifiers from contents or update the "
75
+ "passport data_classification before this call."
76
+ ),
77
+ )
78
+ )
79
+
80
+ if _CROSS_REGION.search(prompt):
81
+ violations.append(
82
+ Violation(
83
+ rule_id="IRIS-XR-001",
84
+ severity=Severity.CRITICAL,
85
+ message=(
86
+ f"Gemini prompt for agent '{passport.name}' references "
87
+ "restricted cross-region geography (China / cn-north)."
88
+ ),
89
+ compliance_refs=["china-pipl:cross-border-transfer"],
90
+ remediation=(
91
+ "Remove cross-region references from contents or document an "
92
+ "approved exception with security."
93
+ ),
94
+ )
95
+ )
96
+
97
+ if _HIGH_RISK_DOMAIN.search(prompt):
98
+ violations.append(
99
+ Violation(
100
+ rule_id="CO-004",
101
+ severity=Severity.HIGH,
102
+ message=(
103
+ f"Gemini prompt for agent '{passport.name}' references a "
104
+ "high-risk consequential domain (loan, diagnosis, or hiring)."
105
+ ),
106
+ compliance_refs=["colorado-ai-act:sb-24-205:consumer-opt-out"],
107
+ remediation=(
108
+ "Set consent evidence in policy context for consequential "
109
+ "processing, or run an IRIS compliance assessment."
110
+ ),
111
+ )
112
+ )
113
+
114
+ return violations
@@ -0,0 +1,5 @@
1
+ """Compatibility shim; use iris_gemini package import."""
2
+
3
+ from iris_gemini import IrisGemini, IrisGeminiAsync
4
+
5
+ __all__ = ["IrisGemini", "IrisGeminiAsync"]
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: iris-security-gemini
3
+ Version: 0.1.0
4
+ Summary: IRIS governance for Google Gemini via google-genai
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: google
14
+ Requires-Dist: google-genai>=0.3; extra == "google"
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-gemini
21
+
22
+ Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
23
+
24
+ Replace one line:
25
+
26
+ ```python
27
+ # client = google.genai.Client()
28
+ client = IrisGemini(passport=passport)
29
+ ```
30
+
31
+ Every `client.models.generate_content()` and `generate_content_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-security-gemini
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ See [examples/governed_gemini.py](examples/governed_gemini.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,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ iris_gemini/__init__.py
4
+ iris_gemini/client.py
5
+ iris_gemini/guardrails.py
6
+ iris_gemini/init.py
7
+ iris_security_gemini.egg-info/PKG-INFO
8
+ iris_security_gemini.egg-info/SOURCES.txt
9
+ iris_security_gemini.egg-info/dependency_links.txt
10
+ iris_security_gemini.egg-info/requires.txt
11
+ iris_security_gemini.egg-info/top_level.txt
12
+ tests/test_gemini_integration.py
@@ -0,0 +1,10 @@
1
+ iris-security-core>=0.1.0
2
+ iris-security-sdk>=0.1.0
3
+
4
+ [dev]
5
+ pytest>=8.0
6
+ pytest-asyncio>=0.23
7
+ ruff>=0.4
8
+
9
+ [google]
10
+ google-genai>=0.3
@@ -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-gemini"
7
+ version = "0.1.0"
8
+ description = "IRIS governance for Google Gemini via google-genai"
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
+ google = ["google-genai>=0.3"]
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]
31
+ packages = ["iris_gemini"]
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,213 @@
1
+ """Integration tests for iris-gemini - mocked google-genai, no network calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import types
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ from iris import IrisViolationError
11
+ from iris_core.engine.cedar import CedarEngine
12
+ from iris_core.evidence.vault import EvidenceVault
13
+ from iris_core.models.passport import AgentPassport, ComplianceTag, DataClassification, Environment
14
+ from iris_gemini import IrisGemini
15
+ from iris_gemini.guardrails import scan_gemini_content
16
+
17
+
18
+ @pytest.fixture
19
+ def compliant_passport():
20
+ return AgentPassport(
21
+ name="gemini-agent",
22
+ owner="team@company.com",
23
+ data_classification=DataClassification.INTERNAL,
24
+ compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
25
+ environments=[Environment.DEV, Environment.PRODUCTION],
26
+ is_high_risk_ai=True,
27
+ evidence_vault_id="vault-abc",
28
+ intent_ref="governance/agents/gemini-agent/policy-intent.md",
29
+ )
30
+
31
+
32
+ @pytest.fixture
33
+ def high_risk_incomplete_passport():
34
+ return AgentPassport(
35
+ name="loan-agent",
36
+ owner="gmoney@gmail.com",
37
+ compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
38
+ environments=[Environment.DEV, Environment.PRODUCTION],
39
+ is_high_risk_ai=True,
40
+ evidence_vault_id=None,
41
+ intent_ref=None,
42
+ )
43
+
44
+
45
+ def _mock_google_genai_module():
46
+ genai_module = MagicMock()
47
+ response = MagicMock()
48
+ response.text = "Hello from Gemini"
49
+ models = MagicMock()
50
+ models.generate_content.return_value = response
51
+ models.generate_content_stream.return_value = iter([response])
52
+ models.generate_content_async = AsyncMock(return_value=response)
53
+ models.generate_content_stream_async = AsyncMock(return_value=response)
54
+
55
+ client = MagicMock()
56
+ client.models = models
57
+ client.api_key = "test-key"
58
+ client.endpoint = "https://generativelanguage.googleapis.com"
59
+ genai_module.Client.return_value = client
60
+
61
+ google_module = types.ModuleType("google")
62
+ google_module.genai = genai_module
63
+ return google_module, genai_module, client
64
+
65
+
66
+ class Part:
67
+ def __init__(self, text):
68
+ self.text = text
69
+
70
+
71
+ class TestIrisGeminiClient:
72
+ def test_client_permits_allowed_call(self, compliant_passport, tmp_path, monkeypatch):
73
+ monkeypatch.setenv("IRIS_ENV", "dev")
74
+ google_module, genai_module, mock_client = _mock_google_genai_module()
75
+ engine = CedarEngine()
76
+ engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
77
+ vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
78
+
79
+ with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
80
+ client = IrisGemini(passport=compliant_passport)
81
+ client._engine = engine
82
+ client._vault = vault
83
+ result = client.models.generate_content(
84
+ model="gemini-2.0-flash",
85
+ contents="Help this customer.",
86
+ )
87
+
88
+ assert result.text == "Hello from Gemini"
89
+ mock_client.models.generate_content.assert_called_once()
90
+ events = vault.get_events()
91
+ assert len(events) == 1
92
+ assert events[0]["decision"] in ("PERMIT", "PERMIT_WITH_WARNINGS")
93
+ assert events[0]["resource"] == "gemini-api/gemini-2.0-flash"
94
+
95
+ def test_client_blocks_in_production(
96
+ self, high_risk_incomplete_passport, tmp_path, monkeypatch
97
+ ):
98
+ monkeypatch.setenv("IRIS_ENV", "production")
99
+ google_module, genai_module, mock_client = _mock_google_genai_module()
100
+ engine = CedarEngine()
101
+ engine.load_policy(
102
+ high_risk_incomplete_passport.agent_id,
103
+ "permit(principal, action, resource);",
104
+ )
105
+ vault = EvidenceVault(
106
+ agent_id=high_risk_incomplete_passport.agent_id,
107
+ vault_dir=tmp_path,
108
+ )
109
+
110
+ with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
111
+ client = IrisGemini(passport=high_risk_incomplete_passport)
112
+ client._engine = engine
113
+ client._vault = vault
114
+ with pytest.raises(IrisViolationError):
115
+ client.models.generate_content(
116
+ model="gemini-2.0-flash",
117
+ contents="Approve this loan application for decisioning.",
118
+ )
119
+
120
+ mock_client.models.generate_content.assert_not_called()
121
+
122
+ def test_client_warns_in_dev(self, high_risk_incomplete_passport, tmp_path, monkeypatch, capsys):
123
+ monkeypatch.setenv("IRIS_ENV", "dev")
124
+ google_module, genai_module, mock_client = _mock_google_genai_module()
125
+ engine = CedarEngine()
126
+ engine.load_policy(
127
+ high_risk_incomplete_passport.agent_id,
128
+ "permit(principal, action, resource);",
129
+ )
130
+ vault = EvidenceVault(
131
+ agent_id=high_risk_incomplete_passport.agent_id,
132
+ vault_dir=tmp_path,
133
+ )
134
+
135
+ with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
136
+ client = IrisGemini(passport=high_risk_incomplete_passport)
137
+ client._engine = engine
138
+ client._vault = vault
139
+ client.models.generate_content(
140
+ model="gemini-2.0-flash",
141
+ contents="Approve this loan application for decisioning.",
142
+ )
143
+
144
+ mock_client.models.generate_content.assert_called_once()
145
+ captured = capsys.readouterr()
146
+ assert "[IRIS WARNING]" in captured.err
147
+
148
+ def test_streaming_intercept(self, compliant_passport, tmp_path, monkeypatch):
149
+ monkeypatch.setenv("IRIS_ENV", "dev")
150
+ google_module, genai_module, mock_client = _mock_google_genai_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", {"google": google_module, "google.genai": genai_module}):
156
+ client = IrisGemini(passport=compliant_passport)
157
+ client._engine = engine
158
+ client._vault = vault
159
+ stream = client.models.generate_content_stream(
160
+ model="gemini-2.0-flash",
161
+ contents="Stream a short response.",
162
+ )
163
+ list(stream)
164
+
165
+ mock_client.models.generate_content_stream.assert_called_once()
166
+ assert len(vault.get_events()) == 1
167
+
168
+ def test_evidence_vault_records_call(self, compliant_passport, tmp_path, monkeypatch):
169
+ monkeypatch.setenv("IRIS_ENV", "dev")
170
+ google_module, genai_module, _ = _mock_google_genai_module()
171
+ engine = CedarEngine()
172
+ engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
173
+ vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
174
+
175
+ with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
176
+ client = IrisGemini(passport=compliant_passport)
177
+ client._engine = engine
178
+ client._vault = vault
179
+ client.models.generate_content(model="gemini-2.0-flash", contents="First call")
180
+
181
+ events = vault.get_events()
182
+ assert len(events) == 1
183
+ assert events[0]["action"] == "call"
184
+ assert events[0]["resource"] == "gemini-api/gemini-2.0-flash"
185
+
186
+ def test_drop_in_replacement_compatibility(self, compliant_passport, monkeypatch):
187
+ monkeypatch.setenv("IRIS_ENV", "dev")
188
+ google_module, genai_module, mock_client = _mock_google_genai_module()
189
+
190
+ with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
191
+ client = IrisGemini(passport=compliant_passport, api_key="test-key")
192
+
193
+ assert client.api_key == "test-key"
194
+ assert client.endpoint == "https://generativelanguage.googleapis.com"
195
+ genai_module.Client.assert_called_once_with(api_key="test-key")
196
+ assert client.models is not None
197
+ assert mock_client.models is not None
198
+
199
+
200
+ class TestGeminiGuardrails:
201
+ def test_pii_detection_in_contents(self, compliant_passport):
202
+ violations = scan_gemini_content(
203
+ [Part("Customer SSN is 123-45-6789 for verification.")],
204
+ compliant_passport,
205
+ )
206
+ assert any(v.rule_id == "IRIS-DATA-001" for v in violations)
207
+
208
+ def test_cross_region_detection(self, compliant_passport):
209
+ violations = scan_gemini_content(
210
+ ["Send this data to beijing data center."],
211
+ compliant_passport,
212
+ )
213
+ assert any(v.rule_id == "IRIS-XR-001" for v in violations)