iris-security-cli 0.2.16__tar.gz → 0.2.18__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.
Files changed (75) hide show
  1. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/PKG-INFO +21 -6
  2. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/README.md +14 -1
  3. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/audit_log.py +10 -5
  4. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/certify.py +4 -2
  5. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/compliance_scan_cmd.py +50 -4
  6. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/cost.py +2 -0
  7. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/drift.py +1 -0
  8. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/evidence.py +6 -1
  9. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/explain.py +1 -1
  10. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/hitl.py +68 -3
  11. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/main.py +17 -8
  12. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/mcp_server.py +86 -0
  13. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/quickstart.py +2 -2
  14. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/scm.py +6 -1
  15. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/users.py +8 -2
  16. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/PKG-INFO +21 -6
  17. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/SOURCES.txt +4 -1
  18. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/requires.txt +2 -2
  19. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/pyproject.toml +7 -5
  20. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_audit_log.py +8 -4
  21. iris_security_cli-0.2.18/tests/test_cli_free_tier_boundary.py +256 -0
  22. iris_security_cli-0.2.18/tests/test_compliance_scan_fail_on.py +113 -0
  23. iris_security_cli-0.2.18/tests/test_docs_pages_check.py +92 -0
  24. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_entitlements_display.py +4 -4
  25. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_evidence.py +44 -45
  26. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_evidence_query.py +5 -3
  27. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_framework_test.py +3 -1
  28. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_hitl_cli.py +54 -0
  29. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_mcp_model_governance.py +3 -2
  30. iris_security_cli-0.2.18/tests/vault_fixtures.py +14 -0
  31. iris_security_cli-0.2.16/tests/test_docs_pages_check.py +0 -45
  32. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/__init__.py +0 -0
  33. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/action_plan.py +0 -0
  34. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/assess.py +0 -0
  35. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/cedar_parser.py +0 -0
  36. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/compiler_config.py +0 -0
  37. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/compliance_check_cmd.py +0 -0
  38. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/compliance_fix.py +0 -0
  39. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/declare.py +0 -0
  40. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/delegation.py +0 -0
  41. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/discover.py +0 -0
  42. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/dlp_cmd.py +0 -0
  43. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/enforce.py +0 -0
  44. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/entitlements_cmd.py +0 -0
  45. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/framework_suggest.py +0 -0
  46. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/framework_test.py +0 -0
  47. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/license_cmd.py +0 -0
  48. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/list_cmd.py +0 -0
  49. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/models_cmd.py +0 -0
  50. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/onboarding_report.py +0 -0
  51. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/org_policy.py +0 -0
  52. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/policy_cache.py +0 -0
  53. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/policy_commit.py +0 -0
  54. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/policy_diff.py +0 -0
  55. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/policy_status.py +0 -0
  56. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/preview.py +0 -0
  57. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/redteam.py +0 -0
  58. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/regulatory.py +0 -0
  59. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/scan_govern.py +0 -0
  60. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/scan_report.py +0 -0
  61. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/sentinel.py +0 -0
  62. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/status_cmd.py +0 -0
  63. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/vault.py +0 -0
  64. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/watch.py +0 -0
  65. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_cli/witness.py +0 -0
  66. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  67. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/entry_points.txt +0 -0
  68. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/iris_security_cli.egg-info/top_level.txt +0 -0
  69. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/setup.cfg +0 -0
  70. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_evidence_export.py +0 -0
  71. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_framework_suggest.py +0 -0
  72. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_list_cmd.py +0 -0
  73. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_policy_diff.py +0 -0
  74. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_quickstart.py +0 -0
  75. {iris_security_cli-0.2.16 → iris_security_cli-0.2.18}/tests/test_vocabulary.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-cli
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
5
5
  License: Apache-2.0
6
- Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
7
- Project-URL: Repository, https://github.com/gimartinb/iris-sdk
6
+ Project-URL: Homepage, https://iris-security.github.io/iris-sdk/
7
+ Project-URL: Repository, https://github.com/IRIS-Security/iris-sdk
8
+ Project-URL: Documentation, https://iris-security.github.io/iris-sdk/cli-reference.html
9
+ Project-URL: Issues, https://github.com/IRIS-Security/iris-sdk/issues
8
10
  Classifier: Development Status :: 3 - Alpha
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: Topic :: Security
@@ -15,8 +17,8 @@ Classifier: Programming Language :: Python :: 3.11
15
17
  Classifier: Programming Language :: Python :: 3.12
16
18
  Requires-Python: >=3.10
17
19
  Description-Content-Type: text/markdown
18
- Requires-Dist: iris-security-core>=0.1.16
19
- Requires-Dist: iris-security-sdk>=0.2.16
20
+ Requires-Dist: iris-security-core>=0.1.18
21
+ Requires-Dist: iris-security-sdk>=0.2.18
20
22
  Requires-Dist: click>=8.1
21
23
  Requires-Dist: rich>=13.0
22
24
  Requires-Dist: pyyaml>=6.0
@@ -29,7 +31,7 @@ IRIS CLI — `iris scan`, `iris register`, `iris policy`, `iris compliance`.
29
31
 
30
32
  Command-line tools for AI agent governance and Colorado AI Act compliance.
31
33
 
32
- Part of the [IRIS SDK](https://github.com/gimartinb/iris-sdk).
34
+ Part of the [IRIS SDK](https://github.com/IRIS-Security/iris-sdk).
33
35
 
34
36
  ## Install
35
37
 
@@ -45,3 +47,16 @@ iris register --name my-agent --owner you@company.com --team my-team --complianc
45
47
  iris scan
46
48
  iris compliance check --framework colorado-ai-act
47
49
  ```
50
+
51
+ ## GitHub Action
52
+
53
+ Add AI governance to CI with the composite Action in this repo (offline by default):
54
+
55
+ ```yaml
56
+ - uses: IRIS-Security/iris-cli@v1
57
+ with:
58
+ command: compliance scan
59
+ fail-on: blocker
60
+ ```
61
+
62
+ Optional `--push` to IRIS Cloud when `api-key` is set. See [GitHub Action docs](https://iris-security.github.io/iris-sdk/github-action.html).
@@ -4,7 +4,7 @@ IRIS CLI — `iris scan`, `iris register`, `iris policy`, `iris compliance`.
4
4
 
5
5
  Command-line tools for AI agent governance and Colorado AI Act compliance.
6
6
 
7
- Part of the [IRIS SDK](https://github.com/gimartinb/iris-sdk).
7
+ Part of the [IRIS SDK](https://github.com/IRIS-Security/iris-sdk).
8
8
 
9
9
  ## Install
10
10
 
@@ -20,3 +20,16 @@ iris register --name my-agent --owner you@company.com --team my-team --complianc
20
20
  iris scan
21
21
  iris compliance check --framework colorado-ai-act
22
22
  ```
23
+
24
+ ## GitHub Action
25
+
26
+ Add AI governance to CI with the composite Action in this repo (offline by default):
27
+
28
+ ```yaml
29
+ - uses: IRIS-Security/iris-cli@v1
30
+ with:
31
+ command: compliance scan
32
+ fail-on: blocker
33
+ ```
34
+
35
+ Optional `--push` to IRIS Cloud when `api-key` is set. See [GitHub Action docs](https://iris-security.github.io/iris-sdk/github-action.html).
@@ -55,6 +55,11 @@ def _event_policy(event: dict) -> str:
55
55
  return "—"
56
56
 
57
57
 
58
+ def _event_r5_satisfied(event: dict) -> bool:
59
+ """R5 tamper-evidence applies only to HMAC-signed vault events (Evidence Vault v2)."""
60
+ return bool(event.get("signature"))
61
+
62
+
58
63
  def _event_hash(event: dict) -> str:
59
64
  if event.get("hash"):
60
65
  return str(event["hash"])
@@ -150,7 +155,7 @@ def _format_splunk(event: dict) -> dict:
150
155
  "iris_regulation": _event_regulation(event),
151
156
  "iris_event_id": event.get("event_id", ""),
152
157
  "iris_hash": _event_hash(event),
153
- "aarm_r5": True,
158
+ "aarm_r5": _event_r5_satisfied(event),
154
159
  "aiuc1_control": _aiuc1_control(event),
155
160
  },
156
161
  }
@@ -195,7 +200,7 @@ def _format_elastic(event: dict) -> dict:
195
200
  "risk.score": _event_risk_score(event),
196
201
  "labels": {
197
202
  "iris_regulation": _event_regulation(event),
198
- "aarm_r5": "true",
203
+ "aarm_r5": "true" if _event_r5_satisfied(event) else "false",
199
204
  },
200
205
  }
201
206
 
@@ -247,7 +252,7 @@ def _format_otel_envelope(events: List[dict]) -> dict:
247
252
  _otel_attribute("service.name", "iris-cli"),
248
253
  _otel_attribute("service.version", IRIS_VERSION),
249
254
  _otel_attribute("iris.vendor", "IRIS Security, Inc."),
250
- _otel_attribute("aarm.conformance", "Core"),
255
+ _otel_attribute("aarm.alignment", "designed-toward"),
251
256
  ],
252
257
  },
253
258
  "scopeSpans": [
@@ -409,7 +414,7 @@ def _print_export_summary(
409
414
  "Formats: Splunk HEC · Datadog · Elastic ECS · OTel OTLP"
410
415
  )
411
416
  err_console.print(
412
- "AARM R8 conformant · https://iris-security.io"
417
+ "OTel export · https://iris-security.io"
413
418
  )
414
419
 
415
420
 
@@ -524,7 +529,7 @@ def audit_log_export(
524
529
  Plug IRIS evidence directly into your existing security stack.
525
530
  No IRIS account required — reads from local Evidence Vault.
526
531
 
527
- AARM R8 conformant: standard telemetry export.
532
+ Exports governance telemetry in standard SIEM formats (including OTel OTLP).
528
533
 
529
534
  Examples:
530
535
  iris audit-log export --format splunk --output events.json
@@ -404,11 +404,13 @@ def certify_cmd(
404
404
 
405
405
  loader = DynamicBundleLoader(pinned_version=registry_version)
406
406
 
407
+ machine_output = output_format in ("json", "markdown", "aiuc1-export")
407
408
  if not offline and not registry_version:
408
409
  if not loader.is_cache_fresh():
409
- console.print("[dim]Checking for regulatory updates...[/dim]")
410
+ if not machine_output:
411
+ console.print("[dim]Checking for regulatory updates...[/dim]")
410
412
  updated = loader.refresh_registry()
411
- if updated:
413
+ if updated and not machine_output:
412
414
  console.print("[green]✓ Regulatory intelligence updated[/green]")
413
415
  console.print("[dim] Run: iris regulatory check for change summary[/dim]")
414
416
 
@@ -7,6 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
+ import sys
10
11
  from pathlib import Path
11
12
 
12
13
  import click
@@ -16,13 +17,29 @@ from rich.table import Table
16
17
 
17
18
  from iris.scan import detect_workload
18
19
  from iris_core.compliance.workload_eval import (
19
- applicable_frameworks,
20
20
  evaluate_workload_profile,
21
+ framework_coverage,
21
22
  top_recommended_actions,
22
23
  )
23
24
 
24
25
  console = Console()
25
26
 
27
+ _SEVERITY_RANK = {"blocker": 0, "required": 1, "recommended": 2}
28
+
29
+
30
+ def _exit_on_fail_threshold(obligations: list[dict], fail_on: str) -> None:
31
+ if fail_on == "none":
32
+ return
33
+ threshold = _SEVERITY_RANK[fail_on]
34
+ blocking = [
35
+ obligation
36
+ for obligation in obligations
37
+ if not obligation.get("informational", False)
38
+ and _SEVERITY_RANK.get(obligation.get("severity", "recommended"), 9) <= threshold
39
+ ]
40
+ if blocking:
41
+ sys.exit(1)
42
+
26
43
 
27
44
  def _resolve_profile(
28
45
  *,
@@ -103,13 +120,32 @@ def _print_profile(profile: dict) -> None:
103
120
 
104
121
 
105
122
  def _print_frameworks(profile: dict) -> None:
123
+ coverage = framework_coverage(profile)
106
124
  table = Table(title="Applicable frameworks", show_header=True, header_style="bold")
107
125
  table.add_column("Framework")
108
- table.add_column("Triggered by")
109
- for fw, reasons in applicable_frameworks(profile):
110
- table.add_row(fw, ", ".join(reasons))
126
+ table.add_column("Why this applies to you")
127
+ table.add_column("Depth")
128
+ for entry in coverage:
129
+ depth = (
130
+ f"[yellow]mapped — thin[/yellow]"
131
+ if entry["thin"]
132
+ else f"{entry['rule_count']} rules"
133
+ )
134
+ table.add_row(entry["bundle_id"], entry["why"], depth)
111
135
  console.print(table)
112
136
 
137
+ thin = [entry for entry in coverage if entry["thin"]]
138
+ if thin:
139
+ console.print(
140
+ "[dim]Thin coverage means the registry has this framework mapped but "
141
+ "not yet deeply evaluated here — run one of the following for a real "
142
+ "control-by-control check:[/dim]"
143
+ )
144
+ for entry in thin:
145
+ console.print(
146
+ f" [dim]•[/dim] iris compliance check --framework {entry['bundle_id']}"
147
+ )
148
+
113
149
 
114
150
  def _print_actions(obligations: list[dict]) -> None:
115
151
  actions = top_recommended_actions(obligations, limit=5)
@@ -166,6 +202,13 @@ def _push_profile(profile: dict) -> None:
166
202
  @click.option("--proxy", "litellm_proxy", help="LiteLLM proxy base URL")
167
203
  @click.option("--lookback-days", default=30, show_default=True, help="Langfuse lookback window")
168
204
  @click.option("--push", is_flag=True, help="POST profile to cloud when IRIS_API_KEY is set")
205
+ @click.option(
206
+ "--fail-on",
207
+ default="none",
208
+ type=click.Choice(["blocker", "required", "none"]),
209
+ show_default=True,
210
+ help="Exit 1 when obligations meet or exceed this severity (CI integration)",
211
+ )
169
212
  @click.option("--json", "as_json", is_flag=True, help="Output raw JSON")
170
213
  def compliance_scan_cmd(
171
214
  path: str,
@@ -174,6 +217,7 @@ def compliance_scan_cmd(
174
217
  litellm_proxy: str | None,
175
218
  lookback_days: int,
176
219
  push: bool,
220
+ fail_on: str,
177
221
  as_json: bool,
178
222
  ) -> None:
179
223
  """Detect workload attributes and evaluate local regulatory obligations (offline)."""
@@ -196,6 +240,7 @@ def compliance_scan_cmd(
196
240
  click.echo(json.dumps({"profile": profile, "obligations": obligations}, indent=2))
197
241
  if push:
198
242
  _push_profile(profile)
243
+ _exit_on_fail_threshold(obligations, fail_on)
199
244
  return
200
245
 
201
246
  console.print(
@@ -214,3 +259,4 @@ def compliance_scan_cmd(
214
259
  )
215
260
  if push:
216
261
  _push_profile(profile)
262
+ _exit_on_fail_threshold(obligations, fail_on)
@@ -18,6 +18,7 @@ from rich.table import Table
18
18
 
19
19
  from iris_core.cost.pricing import DEFAULT_PRICING, overrides_path, PricingRegistry
20
20
  from iris_core.cost.tracker import CostSummary, CostTracker, discover_agent_trackers
21
+ from iris_core.entitlements import Entitlements, Feature
21
22
 
22
23
  console = Console()
23
24
 
@@ -298,6 +299,7 @@ def cost_report(agent: Optional[str], days: int, output_format: str, since: Opti
298
299
  @click.option("--since", default=None, help="ISO date string for period start")
299
300
  def cost_summary(days: int, since: Optional[str]) -> None:
300
301
  """CFO report — cost across all agents sorted by spend."""
302
+ Entitlements().require(Feature.COST_ORG_SUMMARY, context="org-wide cost summary")
301
303
  since_iso = _since_from_date(since, days)
302
304
  trackers = discover_agent_trackers()
303
305
  if not trackers:
@@ -254,6 +254,7 @@ def drift_alert_config(slack_webhook: Optional[str], email: Optional[str], webho
254
254
  _require_pro_license("Email drift alerts", Feature.DRIFT_EMAIL_ALERT)
255
255
  config["email"] = email
256
256
  if webhook:
257
+ _require_pro_license("Webhook drift alerts", Feature.DRIFT_WEBHOOK_ALERT)
257
258
  config["webhook"] = webhook
258
259
 
259
260
  if not any([slack_webhook, email, webhook]):
@@ -981,7 +981,8 @@ def evidence_query(
981
981
  to exactly the events that need attention.
982
982
 
983
983
  This is the CLI equivalent of Evidence Vault Signal mode.
984
- AARM R5 conformant all entries are tamper-evident.
984
+ Local vault events are unsigned JSONL by default; Evidence Vault v2 adds
985
+ HMAC signing and hash chaining for tamper-evident receipts.
985
986
 
986
987
  Examples:
987
988
  iris evidence query --decision deny
@@ -1150,6 +1151,10 @@ def evidence_export(agent, output_path, output_format, governance_dir, vault_dir
1150
1151
  return
1151
1152
 
1152
1153
  if output_format == "oscal":
1154
+ Entitlements().require(
1155
+ Feature.EVIDENCE_EXPORT_AUDITOR,
1156
+ context="evidence export --format oscal (auditor deliverable)",
1157
+ )
1153
1158
  vault = _open_vault(agent, vault_dir)
1154
1159
  vault_data = vault.export_vault()
1155
1160
  oscal_payload = {
@@ -30,7 +30,7 @@ Your existing tests will pass without modification.
30
30
 
31
31
  To verify: run your test suite after adding IRIS.
32
32
  If any test fails, report it as a bug at:
33
- github.com/gimartinb/iris-sdk/issues"""
33
+ github.com/IRIS-Security/iris-sdk/issues"""
34
34
 
35
35
  TECHNICAL_EXPLANATION = """\
36
36
  Proxy pattern (identical attribute forwarding):
@@ -214,6 +214,35 @@ def hitl_setup(agent: str):
214
214
  rules.append(HITLConditionRule(condition=condition, reason=reason, notify_channel=channel))
215
215
  config.condition_rules = rules
216
216
 
217
+ console.print(
218
+ "\nRisk tiers — gate the clearly-irreversible, auto-allow the routine.\n"
219
+ "Leave any of these blank to skip; an empty list never triggers HITL.\n"
220
+ )
221
+ if click.confirm("Require HITL for specific violation severities?", default=False):
222
+ levels = click.prompt(
223
+ "Severities (comma-separated, e.g. CRITICAL,HIGH)", default="CRITICAL"
224
+ )
225
+ config.required_for_risk_levels = [
226
+ lvl.strip().upper() for lvl in levels.split(",") if lvl.strip()
227
+ ]
228
+ if click.confirm(
229
+ "Require HITL for specific actions in staging/production (e.g. write, delete)?",
230
+ default=False,
231
+ ):
232
+ actions = click.prompt("Actions (comma-separated)", default="write,delete")
233
+ config.step_up_actions = [a.strip() for a in actions.split(",") if a.strip()]
234
+ if click.confirm(
235
+ "Require HITL for specific data classifications (e.g. phi, pii)?", default=False
236
+ ):
237
+ classes = click.prompt("Data classifications (comma-separated)", default="phi,pii")
238
+ config.sensitive_data_classifications = [
239
+ c.strip() for c in classes.split(",") if c.strip()
240
+ ]
241
+ config.step_up_on_intent_drift = click.confirm(
242
+ "Require HITL when an action drifts outside the agent's declared intent?",
243
+ default=False,
244
+ )
245
+
217
246
  if click.confirm("Configure Slack webhook URL (Pro)?", default=False):
218
247
  config.slack_webhook_url = click.prompt("Slack webhook URL", default="")
219
248
  if click.confirm("Configure email recipients (Pro)?", default=False):
@@ -223,7 +252,15 @@ def hitl_setup(agent: str):
223
252
  path = _save_hitl_config(agent, config)
224
253
  console.print(f"[green]✓[/green] HITL configured for {agent}")
225
254
  console.print(f"[green]✓[/green] {len(rules)} condition rule(s) declared")
226
- console.print("[green]✓[/green] HITL fires ONLY when a declared rule matches")
255
+ if config.required_for_risk_levels:
256
+ console.print(f"[green]✓[/green] Step-up on severities: {config.required_for_risk_levels}")
257
+ if config.step_up_actions:
258
+ console.print(f"[green]✓[/green] Step-up on actions: {config.step_up_actions}")
259
+ if config.sensitive_data_classifications:
260
+ console.print(f"[green]✓[/green] Step-up on data classifications: {config.sensitive_data_classifications}")
261
+ if config.step_up_on_intent_drift:
262
+ console.print("[green]✓[/green] Step-up on intent drift: enabled")
263
+ console.print("[green]✓[/green] HITL fires ONLY for what you declared above — everything else auto-allows")
227
264
  console.print(f"[green]✓[/green] Saved to {path}")
228
265
 
229
266
 
@@ -255,10 +292,38 @@ def hitl_rules(agent: str):
255
292
  console.print(f" Reason: {entry['reason']}")
256
293
  console.print(" Channel: All channels")
257
294
 
295
+ if config.required_for_risk_levels:
296
+ console.print(
297
+ f"\nWILL trigger HITL — violation severity in "
298
+ f"{config.required_for_risk_levels}:"
299
+ )
300
+ console.print(
301
+ "● Any policy violation at these severities steps up instead of "
302
+ "logging silently or auto-denying"
303
+ )
304
+
305
+ if config.step_up_actions:
306
+ console.print(f"\nWILL trigger HITL — declared step-up actions in staging/production:")
307
+ console.print(f"● {config.step_up_actions}")
308
+
309
+ if config.sensitive_data_classifications:
310
+ console.print(
311
+ f"\nWILL trigger HITL — sensitive data classifications:"
312
+ )
313
+ console.print(f"● {config.sensitive_data_classifications}")
314
+
315
+ if config.step_up_on_intent_drift:
316
+ console.print(
317
+ "\nWILL trigger HITL — action drifts outside the agent's declared "
318
+ "intent (semantic distance > 0.7)"
319
+ )
320
+
258
321
  console.print("\nWILL NOT trigger HITL — everything else:")
259
322
  console.print("✗ Calls with no matching condition rule → automatic")
260
- console.print("✗ Risk level alone → never triggers HITL")
261
- console.print("✗ Data classification alone → never triggers HITL")
323
+ if not config.required_for_risk_levels:
324
+ console.print("✗ Risk level alone → never triggers HITL (declare required_for_risk_levels to change this)")
325
+ if not config.sensitive_data_classifications:
326
+ console.print("✗ Data classification alone → never triggers HITL (declare sensitive_data_classifications to change this)")
262
327
  console.print("\nRun: iris hitl test --agent {agent} to verify.")
263
328
 
264
329
 
@@ -50,10 +50,11 @@ class IrisCLI(click.Group):
50
50
 
51
51
  @click.group(cls=IrisCLI)
52
52
  @click.version_option(
53
- version="0.2.16",
53
+ version="0.2.18",
54
54
  prog_name="iris",
55
- message="%(prog)s %(version)s · AARM Core conformant (R1–R6) · "
56
- "AIUC-1 Q1 2026 aligned · https://iris-security.io",
55
+ message="%(prog)s %(version)s · Evaluates agent actions through an in-process "
56
+ "Cedar engine before adapter-wrapped calls proceed; records decisions "
57
+ "to a local Evidence Vault.",
57
58
  )
58
59
  def cli():
59
60
  """
@@ -485,12 +486,20 @@ framework.add_command(framework_suggest, name="suggest")
485
486
  from iris_cli.assess import compliance_assess
486
487
  compliance.add_command(compliance_assess)
487
488
 
488
- # Wire in evidence commands
489
- from iris_cli.evidence import evidence
490
- cli.add_command(evidence)
489
+ # Wire in evidence commands (optional — requires iris-security-core>=0.1.17 exporters)
490
+ try:
491
+ from iris_cli.evidence import evidence
491
492
 
492
- from iris_cli.vault import vault
493
- cli.add_command(vault)
493
+ cli.add_command(evidence)
494
+ except ModuleNotFoundError:
495
+ pass
496
+
497
+ try:
498
+ from iris_cli.vault import vault
499
+
500
+ cli.add_command(vault)
501
+ except ModuleNotFoundError:
502
+ pass
494
503
 
495
504
  from iris_cli.framework_test import test as framework_test_cmd_legacy
496
505
  # framework_test kept for internal imports; certify is the primary command
@@ -19,6 +19,7 @@ Setup:
19
19
 
20
20
  import json
21
21
  import asyncio
22
+ import re
22
23
  from pathlib import Path
23
24
  from typing import Any
24
25
  from datetime import datetime
@@ -142,6 +143,75 @@ MCP_TOOLS = [
142
143
 
143
144
  # ── Tool handlers ─────────────────────────────────────────────────────────────
144
145
 
146
+ _MODEL_ID_PATTERN = re.compile(r"""model\s*=\s*["']([^"']+)["']""")
147
+
148
+
149
+ def _governance_root(workspace: Path) -> Path | None:
150
+ candidate = workspace / "governance"
151
+ return candidate if candidate.exists() else None
152
+
153
+
154
+ def _model_governance_violations(code: str, workspace: Path) -> list[dict]:
155
+ from iris_core.engine.model_governance import check_model_governance
156
+ from iris_core.models.context import EvaluationContext
157
+ from iris_core.models.directives import DirectiveRegistry
158
+ from iris_core.models.model_registry import ModelRegistry
159
+ from iris_core.models.passport import AgentPassport, Environment
160
+
161
+ model_ids = _MODEL_ID_PATTERN.findall(code)
162
+ if not model_ids:
163
+ return []
164
+
165
+ gov_root = _governance_root(workspace)
166
+ registry = ModelRegistry.load(gov_root)
167
+ directives = DirectiveRegistry.load(gov_root)
168
+ passport = AgentPassport(
169
+ name="mcp-scan",
170
+ owner="unknown@local",
171
+ allowed_model_tiers=["standard", "frontier", "frontier-restricted"],
172
+ )
173
+ violations: list[dict] = []
174
+ for model_id in model_ids:
175
+ ctx = EvaluationContext(
176
+ agent_id="mcp-scan",
177
+ action="call",
178
+ resource="llm-api",
179
+ resource_type="model",
180
+ environment=Environment.DEV,
181
+ model_id=model_id,
182
+ )
183
+ for violation in check_model_governance(passport, ctx, registry, directives):
184
+ violations.append(
185
+ {
186
+ "rule_id": violation.rule_id,
187
+ "severity": violation.severity.value,
188
+ "message": violation.message,
189
+ "compliance_refs": violation.compliance_refs,
190
+ "fix": violation.remediation,
191
+ }
192
+ )
193
+ return violations
194
+
195
+
196
+ def handle_models_status(params: dict) -> dict:
197
+ """Return bundled/user model registry and active directives."""
198
+ from iris_core.models.directives import DirectiveRegistry
199
+ from iris_core.models.model_registry import ModelRegistry
200
+
201
+ workspace = Path(params.get("workspace_path", "."))
202
+ gov_root = _governance_root(workspace)
203
+ registry = ModelRegistry.load(gov_root)
204
+ directives = DirectiveRegistry.load(gov_root)
205
+ return {
206
+ "models": {
207
+ model_id: capability.to_dict()
208
+ for model_id, capability in registry.models.items()
209
+ },
210
+ "active_directives": [d.to_dict() for d in directives.active_directives()],
211
+ "docs": "docs/MODEL_GOVERNANCE.md",
212
+ }
213
+
214
+
145
215
  def handle_check_agent_code(params: dict) -> dict:
146
216
  """
147
217
  Analyze agent code for compliance violations.
@@ -150,6 +220,7 @@ def handle_check_agent_code(params: dict) -> dict:
150
220
  code = params.get("code", "")
151
221
  file_path = params.get("file_path", "unknown")
152
222
  framework = params.get("framework", "colorado-ai-act")
223
+ workspace = Path(params.get("workspace_path", "."))
153
224
 
154
225
  violations = []
155
226
  suggestions = []
@@ -242,6 +313,8 @@ def handle_check_agent_code(params: dict) -> dict:
242
313
  "command": f"iris register --name my-agent --owner you@company.com --compliance colorado-ai-act",
243
314
  })
244
315
 
316
+ violations.extend(_model_governance_violations(code, workspace))
317
+
245
318
  return {
246
319
  "file": file_path,
247
320
  "framework": framework,
@@ -386,6 +459,18 @@ agent = IrisAgent(
386
459
  "explanation": "Declare the destination region so IRIS can enforce cross-region policy",
387
460
  "code_snippet": "@agent.guard(tool=\"storage\", action=\"write\", data_region=\"us-east-1\", destination_region=\"us-east-1\")",
388
461
  },
462
+ "IRIS-MODEL-001": {
463
+ "explanation": "Model is suspended by an active directive — switch to the approved fallback",
464
+ "command": "Review governance/directives/active.yaml and use the fallback_model listed there",
465
+ },
466
+ "IRIS-MODEL-004": {
467
+ "explanation": "Export-control restricted model requires valid user work authorization",
468
+ "code_snippet": "Set user_work_authorization (e.g. us-citizen) in EvaluationContext or use an unrestricted model",
469
+ },
470
+ "IRIS-MODEL-005": {
471
+ "explanation": "Frontier model requires HITL approval in staging/production",
472
+ "code_snippet": "Set hitl_approved=True after security review, or use iris HITL workflow",
473
+ },
389
474
  }
390
475
 
391
476
  fix = fixes.get(rule_id, {
@@ -469,6 +554,7 @@ HANDLERS = {
469
554
  "iris_suggest_policy": handle_suggest_policy,
470
555
  "iris_fix_violation": handle_fix_violation,
471
556
  "iris_scan_workspace": handle_scan_workspace,
557
+ "iris_models_status": handle_models_status,
472
558
  }
473
559
 
474
560
 
@@ -167,7 +167,7 @@ def _print_welcome() -> None:
167
167
  " · How to register and govern a new agent\n"
168
168
  " · Your compliance score in real time\n\n"
169
169
  "No API key required for most steps.\n"
170
- "github.com/gimartinb/iris-sdk",
170
+ "github.com/IRIS-Security/iris-sdk",
171
171
  title="Welcome to IRIS",
172
172
  style="blue",
173
173
  )
@@ -363,7 +363,7 @@ def _print_summary(findings_count: int) -> None:
363
363
  " [cyan]from iris_anthropic import IrisAnthropic[/cyan]\n"
364
364
  " [cyan]client = IrisAnthropic(passport=passport)[/cyan]\n\n"
365
365
  " 4. Read the full guide:\n"
366
- " github.com/gimartinb/iris-sdk/blob/main/QUICKSTART.md\n\n"
366
+ " github.com/IRIS-Security/iris-sdk/blob/main/QUICKSTART.md\n\n"
367
367
  "Questions? gilbert.martin@gmail.com",
368
368
  title="Quickstart complete",
369
369
  style="green",
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
  import click
10
10
  from rich.console import Console
11
11
 
12
+ from iris_core.entitlements import Entitlements, Feature
13
+
12
14
  console = Console()
13
15
 
14
16
 
@@ -94,6 +96,9 @@ def scan_org(
94
96
  export GITHUB_TOKEN=ghp_... && iris scm scan-org --platform github --org my-org
95
97
  iris scm scan-org --platform gitlab --group my-group --token $GITLAB_TOKEN
96
98
  """
99
+ Entitlements().require(Feature.SCM_ORG_SCANNER, context="org-wide SCM scan")
100
+ if comment:
101
+ Entitlements().require(Feature.SCM_PR_COMMENTS, context="SCM PR comments")
97
102
  if platform == "github":
98
103
  if not org:
99
104
  console.print("[red]--org is required for GitHub scans[/red]")
@@ -330,7 +335,7 @@ Step 1: Go to github.com/organizations/{org_name}/settings/apps
330
335
  Step 2: Click "New GitHub App"
331
336
  Step 3: Fill in:
332
337
  Name: IRIS Governance
333
- Homepage URL: https://github.com/gimartinb/iris-sdk
338
+ Homepage URL: https://github.com/IRIS-Security/iris-sdk
334
339
  Webhook: leave unchecked for now
335
340
  Step 4: Set permissions (read-only):
336
341
  Repository permissions:
@@ -8,12 +8,14 @@ import click
8
8
  from rich.console import Console
9
9
  from rich.table import Table
10
10
 
11
+ from iris_core.entitlements import Entitlements, Feature
12
+
11
13
  console = Console()
12
14
 
13
15
 
14
16
  @click.group()
15
17
  def users():
16
- """Manage which users may invoke governed agents (PRO tier RBAC)."""
18
+ """Manage which users may invoke governed agents (Business tier RBAC beyond solo owner)."""
17
19
  pass
18
20
 
19
21
 
@@ -31,7 +33,11 @@ def users_add(email: str, role: str, agent: str, governance_dir: Path):
31
33
  Example:
32
34
  iris users add --email alice@co.com --role developer --agent my-agent
33
35
  """
34
- from iris_core.rbac.registry import add_user
36
+ from iris_core.rbac.registry import add_user, load_users
37
+
38
+ existing = load_users(agent, governance_dir)
39
+ if existing:
40
+ Entitlements().require(Feature.TEAM_RBAC, context="add second+ user to agent")
35
41
 
36
42
  try:
37
43
  entry = add_user(agent, email, role, governance_dir)