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.
- iris_security_openai-0.1.0/PKG-INFO +52 -0
- iris_security_openai-0.1.0/README.md +33 -0
- iris_security_openai-0.1.0/examples/governed_gpt.py +61 -0
- iris_security_openai-0.1.0/iris_openai/__init__.py +46 -0
- iris_security_openai-0.1.0/iris_openai/_governance.py +368 -0
- iris_security_openai-0.1.0/iris_openai/client.py +343 -0
- iris_security_openai-0.1.0/iris_openai/tool_guard.py +97 -0
- iris_security_openai-0.1.0/iris_security_openai.egg-info/PKG-INFO +52 -0
- iris_security_openai-0.1.0/iris_security_openai.egg-info/SOURCES.txt +14 -0
- iris_security_openai-0.1.0/iris_security_openai.egg-info/dependency_links.txt +1 -0
- iris_security_openai-0.1.0/iris_security_openai.egg-info/requires.txt +10 -0
- iris_security_openai-0.1.0/iris_security_openai.egg-info/top_level.txt +4 -0
- iris_security_openai-0.1.0/pyproject.toml +39 -0
- iris_security_openai-0.1.0/setup.cfg +4 -0
- iris_security_openai-0.1.0/tests/conftest.py +11 -0
- iris_security_openai-0.1.0/tests/test_openai_integration.py +322 -0
|
@@ -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)
|