iris-security-openai 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,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: iris-security-openai
3
+ Version: 0.1.0
4
+ Summary: IRIS governance for OpenAI — 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: openai
14
+ Requires-Dist: openai>=1.30; extra == "openai"
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-openai
21
+
22
+ Drop-in IRIS governance for the [OpenAI Python SDK](https://github.com/openai/openai-python).
23
+
24
+ Replace one line:
25
+
26
+ ```python
27
+ # client = openai.OpenAI()
28
+ client = IrisOpenAI(passport=passport)
29
+ ```
30
+
31
+ Every `client.chat.completions.create()`, `stream()`, and `client.embeddings.create()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
32
+
33
+ Tool arrays are filtered to `passport.tool_permissions`; removed tools are logged as `IRIS-TOOL-001` (never silently dropped in dev).
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install iris-openai
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ See [examples/governed_gpt.py](examples/governed_gpt.py).
44
+
45
+ ## Environment
46
+
47
+ | `IRIS_ENV` | Behavior |
48
+ |-------------|-----------------------------------------------|
49
+ | `dev` | Fail open — warnings to stderr, never block |
50
+ | `production`| Fail closed — `IrisViolationError` on deny |
51
+
52
+ Defaults to `dev` when unset.
@@ -0,0 +1,33 @@
1
+ # iris-openai
2
+
3
+ Drop-in IRIS governance for the [OpenAI Python SDK](https://github.com/openai/openai-python).
4
+
5
+ Replace one line:
6
+
7
+ ```python
8
+ # client = openai.OpenAI()
9
+ client = IrisOpenAI(passport=passport)
10
+ ```
11
+
12
+ Every `client.chat.completions.create()`, `stream()`, and `client.embeddings.create()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
13
+
14
+ Tool arrays are filtered to `passport.tool_permissions`; removed tools are logged as `IRIS-TOOL-001` (never silently dropped in dev).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install iris-openai
20
+ ```
21
+
22
+ ## Quickstart
23
+
24
+ See [examples/governed_gpt.py](examples/governed_gpt.py).
25
+
26
+ ## Environment
27
+
28
+ | `IRIS_ENV` | Behavior |
29
+ |-------------|-----------------------------------------------|
30
+ | `dev` | Fail open — warnings to stderr, never block |
31
+ | `production`| Fail closed — `IrisViolationError` on deny |
32
+
33
+ Defaults to `dev` when unset.
@@ -0,0 +1,61 @@
1
+ """
2
+ Governed OpenAI — change one line, keep everything else identical.
3
+
4
+ Requires: pip install iris-openai
5
+ Set IRIS_ENV=dev (default) or production for fail-closed enforcement.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from iris import AgentPassport, ComplianceTag, ToolPermission
11
+ from iris_openai import IrisOpenAI
12
+
13
+ passport = AgentPassport(
14
+ name="analysis-agent",
15
+ owner="team@company.com",
16
+ compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
17
+ tool_permissions=[
18
+ ToolPermission(tool_id="search", description="Web search", allowed_actions=["call"]),
19
+ ],
20
+ )
21
+
22
+ # One line change from: client = openai.OpenAI()
23
+ client = IrisOpenAI(passport=passport)
24
+
25
+ response = client.chat.completions.create(
26
+ model="gpt-4o",
27
+ messages=[{"role": "user", "content": "Analyze this data."}],
28
+ )
29
+ print(response.choices[0].message.content)
30
+
31
+ search_tool = {
32
+ "type": "function",
33
+ "function": {
34
+ "name": "search",
35
+ "description": "Search the web",
36
+ "parameters": {"type": "object", "properties": {}},
37
+ },
38
+ }
39
+ payments_tool = {
40
+ "type": "function",
41
+ "function": {
42
+ "name": "payments",
43
+ "description": "Process payments",
44
+ "parameters": {"type": "object", "properties": {}},
45
+ },
46
+ }
47
+ email_tool = {
48
+ "type": "function",
49
+ "function": {
50
+ "name": "email",
51
+ "description": "Send email",
52
+ "parameters": {"type": "object", "properties": {}},
53
+ },
54
+ }
55
+
56
+ # IRIS filters to permitted tools; payments/email removed if not on passport
57
+ response = client.chat.completions.create(
58
+ model="gpt-4o",
59
+ messages=[{"role": "user", "content": "Look up the account."}],
60
+ tools=[search_tool, payments_tool, email_tool],
61
+ )
@@ -0,0 +1,46 @@
1
+ """
2
+ IRIS OpenAI integration — one-line drop-in for openai.OpenAI().
3
+
4
+ Quickstart:
5
+ from iris_openai import IrisOpenAI
6
+ from iris import AgentPassport, ComplianceTag
7
+
8
+ passport = AgentPassport(
9
+ name="analysis-agent",
10
+ owner="team@company.com",
11
+ compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
12
+ )
13
+ client = IrisOpenAI(passport=passport)
14
+ response = client.chat.completions.create(model="gpt-4o", messages=[...])
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
+ ToolPermission,
26
+ )
27
+ from iris_core.models.policy import Violation
28
+
29
+ from iris_openai.client import IrisAzureOpenAI, IrisOpenAI, IrisOpenAIAsync
30
+ from iris_openai.tool_guard import guard_openai_tools
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "IrisOpenAI",
36
+ "IrisOpenAIAsync",
37
+ "IrisAzureOpenAI",
38
+ "IrisViolationError",
39
+ "AgentPassport",
40
+ "ComplianceTag",
41
+ "DataClassification",
42
+ "Environment",
43
+ "ToolPermission",
44
+ "Violation",
45
+ "guard_openai_tools",
46
+ ]
@@ -0,0 +1,368 @@
1
+ """Shared IRIS evaluation helpers for OpenAI 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 Any, Dict, List, Optional
11
+ from urllib.parse import urlparse
12
+
13
+ import yaml
14
+
15
+ from iris import IrisViolationError
16
+ from iris_core.engine.cedar import CedarEngine, EvaluationContext
17
+ from iris_core.rbac.context import UserContext
18
+ from iris_core.evidence.vault import EvidenceVault
19
+ from iris_core.models.passport import AgentPassport, Environment
20
+ from iris_core.models.policy import PolicyResult, Severity, Violation
21
+ from iris_core.models.region import RegionPolicy, TransferRule
22
+
23
+ logger = logging.getLogger("iris.openai")
24
+
25
+ _VAULT_LOCK = threading.Lock()
26
+
27
+ _AZURE_LOCATION_TOKENS: Dict[str, str] = {
28
+ "westeurope": "eu-west-1",
29
+ "northeurope": "eu-north-1",
30
+ "swedencentral": "eu-north-1",
31
+ "francecentral": "eu-central-1",
32
+ "germanywestcentral": "eu-central-1",
33
+ "eastus": "us-east-1",
34
+ "eastus2": "us-east-2",
35
+ "westus": "us-west-1",
36
+ "westus2": "us-west-2",
37
+ "centralus": "us-central-1",
38
+ "southcentralus": "us-central-1",
39
+ }
40
+
41
+ _EU_REGION_PREFIXES = ("eu-",)
42
+
43
+
44
+ def current_environment() -> Environment:
45
+ return Environment(os.environ.get("IRIS_ENV", "dev"))
46
+
47
+
48
+ def has_policy_loaded(engine: CedarEngine, passport: AgentPassport) -> bool:
49
+ return bool(engine._policy_cache.get(passport.agent_id))
50
+
51
+
52
+ def load_passport_policy(engine: CedarEngine, passport: AgentPassport) -> None:
53
+ if not passport.policy_ref:
54
+ return
55
+ policy_path = Path(passport.policy_ref)
56
+ if not policy_path.is_absolute():
57
+ policy_path = Path.cwd() / policy_path
58
+ if policy_path.exists():
59
+ engine.load_policy_file(passport.agent_id, policy_path)
60
+
61
+
62
+ def load_region_policy() -> Optional[RegionPolicy]:
63
+ """Load optional RegionPolicy from GitOps or ~/.iris."""
64
+ candidates = [
65
+ Path.cwd() / "governance" / "region-policy.yaml",
66
+ Path.home() / ".iris" / "region-policy.yaml",
67
+ ]
68
+ env_path = os.environ.get("IRIS_REGION_POLICY")
69
+ if env_path:
70
+ candidates.insert(0, Path(env_path))
71
+
72
+ for path in candidates:
73
+ if not path.exists():
74
+ continue
75
+ data = yaml.safe_load(path.read_text()) or {}
76
+ spec = data.get("spec", data)
77
+ transfers = []
78
+ for rule in spec.get("restricted_transfers", []):
79
+ transfers.append(
80
+ TransferRule(
81
+ from_region=rule["from_region"],
82
+ to_region=rule["to_region"],
83
+ compliance_ref=rule.get("compliance_ref", "iris:cross-region"),
84
+ action=rule.get("action", "block"),
85
+ note=rule.get("note"),
86
+ )
87
+ )
88
+ return RegionPolicy(
89
+ name=spec.get("name", data.get("metadata", {}).get("name", "default")),
90
+ restricted_transfers=transfers,
91
+ )
92
+ return None
93
+
94
+
95
+ def parse_azure_endpoint_region(azure_endpoint: Optional[str]) -> Optional[str]:
96
+ """
97
+ Extract a canonical region from an Azure OpenAI endpoint URL.
98
+
99
+ https://my-resource.openai.azure.com → no region in hostname
100
+ https://my-resource.cognitiveservices.azure.com/openai → location token in host
101
+ """
102
+ if not azure_endpoint:
103
+ return None
104
+ host = (urlparse(azure_endpoint).hostname or "").lower()
105
+ if not host:
106
+ return None
107
+ if host.endswith(".openai.azure.com"):
108
+ return None
109
+ for token, region in _AZURE_LOCATION_TOKENS.items():
110
+ if token in host:
111
+ return region
112
+ for part in host.split("."):
113
+ if part.startswith(("eu-", "us-", "ap-", "cn-")):
114
+ return part
115
+ return None
116
+
117
+
118
+ def is_eu_region(region: Optional[str]) -> bool:
119
+ return bool(region and region.startswith(_EU_REGION_PREFIXES))
120
+
121
+
122
+ def check_region_policy_transfer(
123
+ region_policy: RegionPolicy,
124
+ data_region: str,
125
+ destination_region: str,
126
+ ) -> Optional[Violation]:
127
+ for rule in region_policy.restricted_transfers:
128
+ if rule.from_region == data_region and rule.to_region == destination_region:
129
+ if rule.action == "block":
130
+ return Violation(
131
+ rule_id="IRIS-XR-001",
132
+ severity=Severity.CRITICAL,
133
+ message=(
134
+ f"Cross-region data transfer blocked by region policy "
135
+ f"'{region_policy.name}': {data_region} → {destination_region}. "
136
+ f"{rule.note or ''}".strip()
137
+ ),
138
+ compliance_refs=[rule.compliance_ref],
139
+ remediation=(
140
+ "Use an Azure endpoint in an approved region or update "
141
+ "governance/region-policy.yaml with a documented exception."
142
+ ),
143
+ )
144
+ return None
145
+
146
+
147
+ def azure_cross_region_violation(
148
+ passport: AgentPassport,
149
+ azure_endpoint: Optional[str],
150
+ region_policy: Optional[RegionPolicy] = None,
151
+ ) -> Optional[Violation]:
152
+ """Flag EU/US mismatches when Azure endpoint region is known."""
153
+ destination = parse_azure_endpoint_region(azure_endpoint)
154
+ if not destination:
155
+ return None
156
+
157
+ data_region = passport.allowed_regions[0] if passport.allowed_regions else None
158
+ if not data_region:
159
+ if is_eu_region(destination):
160
+ return Violation(
161
+ rule_id="IRIS-XR-002",
162
+ severity=Severity.HIGH,
163
+ message=(
164
+ f"Azure OpenAI endpoint resolves to EU region '{destination}' "
165
+ f"but agent '{passport.name}' has no allowed_regions declared. "
166
+ f"EU-hosted inference may process EU personal data under GDPR."
167
+ ),
168
+ compliance_refs=["gdpr:chapter-5-transfer", "iris:cross-region"],
169
+ remediation=(
170
+ "Set allowed_regions on the agent passport to document approved "
171
+ "data residency, or use a non-EU Azure endpoint."
172
+ ),
173
+ )
174
+ return None
175
+
176
+ policy = region_policy or load_region_policy()
177
+ if policy:
178
+ violation = check_region_policy_transfer(policy, data_region, destination)
179
+ if violation:
180
+ return violation
181
+
182
+ if is_eu_region(data_region) != is_eu_region(destination):
183
+ return Violation(
184
+ rule_id="IRIS-XR-001",
185
+ severity=Severity.CRITICAL,
186
+ message=(
187
+ f"Cross-region Azure OpenAI call blocked: passport data region "
188
+ f"'{data_region}' does not match endpoint region '{destination}'."
189
+ ),
190
+ compliance_refs=["gdpr:chapter-5-transfer", "iris:cross-region"],
191
+ remediation=(
192
+ "Point azure_endpoint at a region that matches the agent's "
193
+ "allowed_regions, or update the passport after security review."
194
+ ),
195
+ )
196
+ return None
197
+
198
+
199
+ def apply_no_policy_gate(
200
+ engine: CedarEngine,
201
+ passport: AgentPassport,
202
+ env: Environment,
203
+ result: PolicyResult,
204
+ ) -> PolicyResult:
205
+ if has_policy_loaded(engine, passport):
206
+ return result
207
+ if env in (Environment.DEV, Environment.TEST):
208
+ if result.decision == "DENY":
209
+ return PolicyResult(
210
+ decision="PERMIT_WITH_WARNINGS",
211
+ violations=result.violations,
212
+ agent_id=result.agent_id,
213
+ action=result.action,
214
+ resource=result.resource,
215
+ environment=result.environment,
216
+ )
217
+ return result
218
+
219
+
220
+ def merge_results(base: PolicyResult, extra_violations: List[Violation]) -> PolicyResult:
221
+ if not extra_violations:
222
+ return base
223
+ violations = list(base.violations) + list(extra_violations)
224
+ critical = [v for v in violations if v.severity == Severity.CRITICAL]
225
+ high = [v for v in violations if v.severity in (Severity.HIGH, Severity.CRITICAL)]
226
+ if critical:
227
+ decision = "DENY"
228
+ elif high and base.decision == "PERMIT":
229
+ decision = "DENY"
230
+ elif violations and base.decision == "PERMIT":
231
+ decision = "PERMIT_WITH_WARNINGS"
232
+ else:
233
+ decision = base.decision
234
+ return PolicyResult(
235
+ decision=decision,
236
+ violations=violations,
237
+ agent_id=base.agent_id,
238
+ action=base.action,
239
+ resource=base.resource,
240
+ environment=base.environment,
241
+ )
242
+
243
+
244
+ def evaluate_openai_call(
245
+ engine: CedarEngine,
246
+ vault: EvidenceVault,
247
+ passport: AgentPassport,
248
+ env: Environment,
249
+ *,
250
+ resource: str = "openai-api",
251
+ operation: str = "chat.completions",
252
+ model: Optional[str] = None,
253
+ tool_names: Optional[List[str]] = None,
254
+ data_classification: Optional[str] = None,
255
+ azure_endpoint: Optional[str] = None,
256
+ extra_violations: Optional[List[Violation]] = None,
257
+ dlp_prompt_findings: Optional[list] = None,
258
+ user_email: Optional[str] = None,
259
+ user_role: Optional[str] = None,
260
+ ) -> PolicyResult:
261
+ data_region = passport.allowed_regions[0] if passport.allowed_regions else None
262
+ destination_region = parse_azure_endpoint_region(azure_endpoint)
263
+ user_ctx = UserContext.from_params(user_email, user_role)
264
+ user_fields = user_ctx.evaluation_fields()
265
+
266
+ ctx = EvaluationContext(
267
+ agent_id=passport.agent_id,
268
+ action="call",
269
+ resource=resource,
270
+ resource_type="api",
271
+ environment=env,
272
+ data_region=data_region,
273
+ destination_region=destination_region,
274
+ data_classification=data_classification or passport.data_classification.value,
275
+ dlp_prompt_findings=dlp_prompt_findings,
276
+ additional={
277
+ "operation": operation,
278
+ "model": model,
279
+ "tool_names": tool_names or [],
280
+ "azure_endpoint": azure_endpoint,
281
+ },
282
+ **user_fields,
283
+ )
284
+
285
+ result = engine.evaluate(passport, ctx)
286
+ result = apply_no_policy_gate(engine, passport, env, result)
287
+
288
+ violations: List[Violation] = list(extra_violations or [])
289
+ azure_v = azure_cross_region_violation(passport, azure_endpoint)
290
+ if azure_v:
291
+ violations.append(azure_v)
292
+
293
+ for name in tool_names or []:
294
+ tool_ctx = EvaluationContext(
295
+ agent_id=passport.agent_id,
296
+ action="call",
297
+ resource=name,
298
+ resource_type="tool",
299
+ environment=env,
300
+ data_classification=data_classification or passport.data_classification.value,
301
+ **user_fields,
302
+ )
303
+ tool_result = engine.evaluate(passport, tool_ctx)
304
+ tool_result = apply_no_policy_gate(engine, passport, env, tool_result)
305
+ violations.extend(tool_result.violations)
306
+ if tool_result.decision == "DENY":
307
+ result = PolicyResult(
308
+ decision="DENY",
309
+ violations=list(result.violations) + violations,
310
+ agent_id=result.agent_id,
311
+ action=result.action,
312
+ resource=result.resource,
313
+ environment=result.environment,
314
+ )
315
+
316
+ if env == Environment.PRODUCTION and (tool_names or []) and not passport.tool_permissions:
317
+ for name in tool_names:
318
+ violations.append(
319
+ Violation(
320
+ rule_id="IRIS-TOOL-001",
321
+ severity=Severity.CRITICAL,
322
+ message=(
323
+ f"Agent '{passport.name}' invoked tool '{name}' in production "
324
+ f"with no tool_permissions declared on the passport."
325
+ ),
326
+ compliance_refs=["iris:tool-permission", "colorado-ai-act:transparency"],
327
+ remediation=(
328
+ "Declare permitted tools in passport.yaml tool_permissions "
329
+ "before enabling tools in production."
330
+ ),
331
+ )
332
+ )
333
+ result = PolicyResult(
334
+ decision="DENY",
335
+ violations=list(result.violations) + violations,
336
+ agent_id=result.agent_id,
337
+ action=result.action,
338
+ resource=result.resource,
339
+ environment=result.environment,
340
+ )
341
+
342
+ result = merge_results(result, violations)
343
+
344
+ with _VAULT_LOCK:
345
+ vault.record(ctx, result)
346
+ return result
347
+
348
+
349
+ def enforce_result(result: PolicyResult, env: Environment) -> None:
350
+ if result.decision == "DENY":
351
+ if env in (Environment.DEV, Environment.TEST):
352
+ for violation in result.violations:
353
+ msg = (
354
+ f"[IRIS WARNING] {violation.message} "
355
+ f"Remediation: {violation.remediation}"
356
+ )
357
+ logger.warning(msg)
358
+ print(msg, file=sys.stderr)
359
+ return
360
+ raise IrisViolationError(result)
361
+ if result.decision == "PERMIT_WITH_WARNINGS":
362
+ for violation in result.violations:
363
+ msg = (
364
+ f"[IRIS WARNING] {violation.message} "
365
+ f"Remediation: {violation.remediation}"
366
+ )
367
+ logger.warning(msg)
368
+ print(msg, file=sys.stderr)