iris-security-sdk 0.1.7__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-sdk
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: IRIS — AI Agent Governance SDK. Govern AI agents locally.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
- Requires-Dist: iris-security-core>=0.1.6
19
+ Requires-Dist: iris-security-core>=0.1.8
20
20
  Requires-Dist: pyyaml>=6.0
21
21
  Requires-Dist: pydantic>=2.0
22
22
  Requires-Dist: click>=8.1
@@ -34,17 +34,20 @@ from iris_core.models.passport import (
34
34
  Environment,
35
35
  ComplianceTag,
36
36
  ToolPermission,
37
+ UserContext,
37
38
  )
39
+ from iris_core.hitl.models import HITLConfig, HITLConditionRule, HITLStatus
40
+ from iris_core.hitl.error import IrisHITLRequiredError
41
+ from iris.hitl import HITLPoller
38
42
  from iris_core.models.policy import PolicyResult, Violation, Severity
39
43
  from iris_core.engine.cedar import CedarEngine, EvaluationContext
40
- from iris_core.rbac.context import UserContext
41
44
  from iris_core.engine.compiler import PolicyCompiler, CompilationResult
42
45
  from iris_core.compliance.registry import ComplianceRegistry
43
46
  from iris_core.evidence.vault import EvidenceVault
44
47
  from iris_core.cost.tracker import CostSummary, CostTracker, CostEntry
45
48
  from iris_core.cost.pricing import PricingRegistry
46
49
 
47
- __version__ = "0.1.7"
50
+ __version__ = "0.2.0"
48
51
  __all__ = [
49
52
  # Main classes
50
53
  "IrisAgent",
@@ -56,6 +59,7 @@ __all__ = [
56
59
  "Environment",
57
60
  "ComplianceTag",
58
61
  "ToolPermission",
62
+ "UserContext",
59
63
  "EvaluationContext",
60
64
  "PolicyResult",
61
65
  "Violation",
@@ -72,6 +76,11 @@ __all__ = [
72
76
  "PricingRegistry",
73
77
  "IrisViolationError",
74
78
  "IrisCrisisDetectedError",
79
+ "IrisHITLRequiredError",
80
+ "HITLConfig",
81
+ "HITLConditionRule",
82
+ "HITLStatus",
83
+ "HITLPoller",
75
84
  ]
76
85
 
77
86
 
@@ -119,11 +128,15 @@ class IrisAgent:
119
128
  environment: Optional[str] = None,
120
129
  user_email: Optional[str] = None,
121
130
  user_role: Optional[str] = None,
131
+ user_context: Optional[UserContext] = None,
122
132
  ):
123
133
  compliance_tags = [ComplianceTag(c) for c in (compliance or [])]
124
134
  envs = [Environment(e) for e in (environments or ["dev", "test", "staging", "production"])]
125
135
  current_env = Environment(environment or os.environ.get("IRIS_ENV", "dev"))
126
- self._user_ctx = UserContext.from_params(user_email, user_role)
136
+ self._user_ctx = user_context or UserContext.from_params(
137
+ user_email=user_email, user_role=user_role
138
+ )
139
+ self._default_user_context = user_context
127
140
 
128
141
  self.passport = AgentPassport(
129
142
  name=name,
@@ -190,6 +203,7 @@ class IrisAgent:
190
203
  data_region: Optional[str] = None,
191
204
  destination_region: Optional[str] = None,
192
205
  data_classification: Optional[str] = None,
206
+ user_context: Optional[UserContext] = None,
193
207
  ) -> Callable:
194
208
  """
195
209
  Decorator that intercepts function calls and evaluates them against policy.
@@ -205,6 +219,10 @@ class IrisAgent:
205
219
  def decorator(func: Callable) -> Callable:
206
220
  @wraps(func)
207
221
  def wrapper(*args, **kwargs) -> Any:
222
+ effective_user = user_context or self._default_user_context
223
+ user_fields = self._user_ctx.evaluation_fields()
224
+ if effective_user:
225
+ user_fields = effective_user.evaluation_fields()
208
226
  ctx = EvaluationContext(
209
227
  agent_id=self.passport.agent_id,
210
228
  action=action,
@@ -214,7 +232,8 @@ class IrisAgent:
214
232
  data_region=data_region,
215
233
  destination_region=destination_region,
216
234
  data_classification=data_classification,
217
- **self._user_ctx.evaluation_fields(),
235
+ user_context=effective_user,
236
+ **user_fields,
218
237
  )
219
238
  result = self.evaluate(ctx)
220
239
 
@@ -231,7 +250,7 @@ class IrisAgent:
231
250
  def evaluate(self, context: EvaluationContext) -> PolicyResult:
232
251
  """Direct policy evaluation without the decorator pattern."""
233
252
  result = self._engine.evaluate(self.passport, context)
234
- self._vault.record(context, result)
253
+ self._vault.record(context, result, passport=self.passport)
235
254
  from iris._telemetry import maybe_fire_first_policy_run
236
255
 
237
256
  maybe_fire_first_policy_run()
@@ -278,8 +297,11 @@ class IrisViolationError(Exception):
278
297
  explanation of what was blocked and how to remediate.
279
298
  """
280
299
 
281
- def __init__(self, result: PolicyResult):
300
+ def __init__(self, result: PolicyResult, is_compliance_block: bool = False):
282
301
  self.result = result
302
+ self.is_compliance_block = is_compliance_block or getattr(
303
+ result, "is_compliance_block", False
304
+ )
283
305
  primary = result.violations[0] if result.violations else None
284
306
  message = (
285
307
  f"\n[IRIS POLICY VIOLATION]\n"
@@ -1,4 +1,7 @@
1
- """First-run telemetry for IRIS SDK installs. Opt-out via IRIS_TELEMETRY_OPT_OUT=1."""
1
+ """First-run telemetry for IRIS SDK installs. Opt-out via IRIS_TELEMETRY_OPT_OUT=1.
2
+
3
+ Each event includes an ISO 8601 UTC timestamp for server-side DoD/WoW/MoM aggregation.
4
+ """
2
5
 
3
6
  from __future__ import annotations
4
7
 
@@ -19,6 +22,35 @@ _FIRST_RUN_SENTINEL = _IRIS_DIR / ".telemetry_sent"
19
22
  _FIRST_POLICY_SENTINEL = _IRIS_DIR / ".first_policy_sent"
20
23
 
21
24
 
25
+ def detect_country() -> str:
26
+ """Infer ISO 3166-1 alpha-2 country from system locale (not GPS or IP)."""
27
+ candidates: list[str] = []
28
+ for env in ("LC_ALL", "LC_CTYPE", "LANG"):
29
+ val = os.environ.get(env, "").strip()
30
+ if val:
31
+ candidates.append(val)
32
+
33
+ try:
34
+ import locale
35
+
36
+ for getter in (locale.getlocale, locale.getdefaultlocale):
37
+ try:
38
+ loc = getter()
39
+ if loc and loc[0]:
40
+ candidates.append(loc[0])
41
+ except Exception:
42
+ pass
43
+ except Exception:
44
+ pass
45
+
46
+ for tag in candidates:
47
+ normalized = tag.split(".")[0].replace("-", "_")
48
+ parts = normalized.split("_")
49
+ if len(parts) >= 2 and len(parts[-1]) == 2 and parts[-1].isalpha():
50
+ return parts[-1].upper()
51
+ return "unknown"
52
+
53
+
22
54
  def detect_install_method() -> str:
23
55
  """Detect how the iris CLI was installed from the executable path."""
24
56
  iris_path = shutil.which("iris")
@@ -69,6 +101,7 @@ def _build_payload(event: str) -> dict[str, str]:
69
101
  "event": event,
70
102
  "install_id": _get_or_create_install_id(),
71
103
  "install_method": detect_install_method(),
104
+ "country": detect_country(),
72
105
  "python_version": sys.version,
73
106
  "platform": sys.platform,
74
107
  "iris_version": _get_iris_version(),
@@ -0,0 +1,81 @@
1
+ """HITL polling helper for application-layer wait loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Callable, Optional
8
+
9
+ from iris_core.hitl.models import HITLStatus
10
+ from iris_core.hitl.queue import HITLQueue
11
+
12
+
13
+ class HITLPoller:
14
+ """
15
+ Helper for applications that need to poll for HITL resolution.
16
+ Handles the polling loop, timeout, and retry logic.
17
+ """
18
+
19
+ def __init__(self, review_id: str, queue: Optional[HITLQueue] = None):
20
+ self.review_id = review_id
21
+ self._queue = queue or HITLQueue()
22
+
23
+ def wait(
24
+ self,
25
+ timeout: int = 300,
26
+ poll_interval: int = 5,
27
+ on_status_change: Optional[Callable[[HITLStatus], None]] = None,
28
+ ) -> HITLStatus:
29
+ deadline = time.time() + timeout
30
+ last_status: Optional[HITLStatus] = None
31
+ while time.time() < deadline:
32
+ review = self._queue.get(self.review_id)
33
+ if review is None:
34
+ time.sleep(poll_interval)
35
+ continue
36
+ if review.status != last_status:
37
+ if on_status_change:
38
+ on_status_change(review.status)
39
+ last_status = review.status
40
+ if review.status in (
41
+ HITLStatus.APPROVED,
42
+ HITLStatus.REJECTED,
43
+ HITLStatus.TIMED_OUT,
44
+ HITLStatus.ESCALATED,
45
+ ):
46
+ return review.status
47
+ if self._queue.is_expired(review):
48
+ resolved = self._queue.apply_timeout_policy(review)
49
+ return resolved.status
50
+ time.sleep(poll_interval)
51
+ return HITLStatus.TIMED_OUT
52
+
53
+ async def wait_async(
54
+ self,
55
+ timeout: int = 300,
56
+ poll_interval: int = 5,
57
+ on_status_change: Optional[Callable[[HITLStatus], None]] = None,
58
+ ) -> HITLStatus:
59
+ deadline = time.time() + timeout
60
+ last_status: Optional[HITLStatus] = None
61
+ while time.time() < deadline:
62
+ review = self._queue.get(self.review_id)
63
+ if review is None:
64
+ await asyncio.sleep(poll_interval)
65
+ continue
66
+ if review.status != last_status:
67
+ if on_status_change:
68
+ on_status_change(review.status)
69
+ last_status = review.status
70
+ if review.status in (
71
+ HITLStatus.APPROVED,
72
+ HITLStatus.REJECTED,
73
+ HITLStatus.TIMED_OUT,
74
+ HITLStatus.ESCALATED,
75
+ ):
76
+ return review.status
77
+ if self._queue.is_expired(review):
78
+ resolved = self._queue.apply_timeout_policy(review)
79
+ return resolved.status
80
+ await asyncio.sleep(poll_interval)
81
+ return HITLStatus.TIMED_OUT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-sdk
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: IRIS — AI Agent Governance SDK. Govern AI agents locally.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
- Requires-Dist: iris-security-core>=0.1.6
19
+ Requires-Dist: iris-security-core>=0.1.8
20
20
  Requires-Dist: pyyaml>=6.0
21
21
  Requires-Dist: pydantic>=2.0
22
22
  Requires-Dist: click>=8.1
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  iris/__init__.py
4
4
  iris/_telemetry.py
5
5
  iris/_telemetry_config.py
6
+ iris/hitl.py
6
7
  iris_security_sdk.egg-info/PKG-INFO
7
8
  iris_security_sdk.egg-info/SOURCES.txt
8
9
  iris_security_sdk.egg-info/dependency_links.txt
@@ -1,4 +1,4 @@
1
- iris-security-core>=0.1.6
1
+ iris-security-core>=0.1.8
2
2
  pyyaml>=6.0
3
3
  pydantic>=2.0
4
4
  click>=8.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iris-security-sdk"
7
- version = "0.1.7"
7
+ version = "0.2.0"
8
8
  description = "IRIS — AI Agent Governance SDK. Govern AI agents locally."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Programming Language :: Python :: 3.12",
29
29
  ]
30
30
  dependencies = [
31
- "iris-security-core>=0.1.6",
31
+ "iris-security-core>=0.1.8",
32
32
  "pyyaml>=6.0",
33
33
  "pydantic>=2.0",
34
34
  "click>=8.1",
@@ -65,6 +65,7 @@ Repository = "https://github.com/gimartinb/iris-sdk"
65
65
 
66
66
  [tool.setuptools.packages.find]
67
67
  where = ["."]
68
+ exclude = ["tools*", "demo*", "docs*"]
68
69
 
69
70
  [tool.ruff]
70
71
  line-length = 100