iris-security-cli 0.1.3__tar.gz → 0.1.7__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 (43) hide show
  1. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/PKG-INFO +3 -3
  2. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/assess.py +66 -19
  3. iris_security_cli-0.1.7/iris_cli/compliance_check_cmd.py +127 -0
  4. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/drift.py +11 -12
  5. iris_security_cli-0.1.7/iris_cli/entitlements_cmd.py +16 -0
  6. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/evidence.py +8 -6
  7. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/framework_suggest.py +99 -5
  8. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/framework_test.py +151 -25
  9. iris_security_cli-0.1.7/iris_cli/license_cmd.py +153 -0
  10. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/main.py +24 -146
  11. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/mcp_server.py +1 -1
  12. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/redteam.py +2 -2
  13. iris_security_cli-0.1.7/iris_cli/regulatory.py +365 -0
  14. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/status_cmd.py +3 -0
  15. iris_security_cli-0.1.7/iris_cli/vault.py +219 -0
  16. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/PKG-INFO +3 -3
  17. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/SOURCES.txt +6 -0
  18. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/requires.txt +2 -2
  19. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/pyproject.toml +3 -3
  20. iris_security_cli-0.1.7/tests/test_entitlements_display.py +135 -0
  21. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/tests/test_framework_suggest.py +1 -1
  22. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/tests/test_framework_test.py +1 -1
  23. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/README.md +0 -0
  24. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/__init__.py +0 -0
  25. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/action_plan.py +0 -0
  26. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/cedar_parser.py +0 -0
  27. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/compiler_config.py +0 -0
  28. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/cost.py +0 -0
  29. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/dlp_cmd.py +0 -0
  30. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/explain.py +0 -0
  31. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/policy_cache.py +0 -0
  32. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/policy_diff.py +0 -0
  33. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/scan_govern.py +0 -0
  34. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/scan_report.py +0 -0
  35. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/scm.py +0 -0
  36. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/users.py +0 -0
  37. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_cli/watch.py +0 -0
  38. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  39. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/entry_points.txt +0 -0
  40. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/top_level.txt +0 -0
  41. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/setup.cfg +0 -0
  42. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/tests/test_evidence.py +0 -0
  43. {iris_security_cli-0.1.3 → iris_security_cli-0.1.7}/tests/test_policy_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-cli
3
- Version: 0.1.3
3
+ Version: 0.1.7
4
4
  Summary: IRIS CLI — iris scan, iris register, iris policy, iris compliance
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: iris-security-core>=0.1.2
19
- Requires-Dist: iris-security-sdk>=0.1.3
18
+ Requires-Dist: iris-security-core>=0.1.6
19
+ Requires-Dist: iris-security-sdk>=0.1.7
20
20
  Requires-Dist: click>=8.1
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: pyyaml>=6.0
@@ -21,13 +21,13 @@ from rich.console import Console
21
21
  from rich.panel import Panel
22
22
  from rich.prompt import Prompt, Confirm
23
23
  from rich.table import Table
24
- from rich import print as rprint
24
+ from iris_core import __version__ as IRIS_PLATFORM_VERSION
25
25
 
26
26
  console = Console()
27
27
 
28
28
 
29
29
  # ── Colorado AI Act impact assessment questionnaire ───────────────────────────
30
- # Each question maps to a specific obligation under SB 24-205.
30
+ # Each question maps to a specific obligation under SB 26-189.
31
31
  # The answers drive the risk level and the generated assessment document.
32
32
 
33
33
  QUESTIONNAIRE = [
@@ -44,7 +44,7 @@ QUESTIONNAIRE = [
44
44
  "insurance",
45
45
  "legal services",
46
46
  "government",
47
- "other (not high-risk)",
47
+ "other (not covered ADMT)",
48
48
  ],
49
49
  "type": "choice",
50
50
  "high_risk_values": [
@@ -58,6 +58,27 @@ QUESTIONNAIRE = [
58
58
  "government",
59
59
  ],
60
60
  },
61
+ {
62
+ "id": "impact_assessment",
63
+ "rule": "CO-002",
64
+ "severity": "MEDIUM",
65
+ "question": (
66
+ "Has an impact assessment been completed for this agent? "
67
+ "(Not legally required under SB 26-189 but recommended as "
68
+ "best practice for NIST AI RMF alignment)"
69
+ ),
70
+ "type": "confirm",
71
+ },
72
+ {
73
+ "id": "record_retention",
74
+ "rule": "CO-RR-001",
75
+ "severity": "HIGH",
76
+ "question": (
77
+ "Do you have a plan for retaining AI usage records for 3 years? "
78
+ "(Required under SB 26-189 for covered ADMT systems)"
79
+ ),
80
+ "type": "confirm",
81
+ },
61
82
  {
62
83
  "id": "consequential",
63
84
  "rule": "CO-004",
@@ -137,7 +158,8 @@ def run_questionnaire() -> dict:
137
158
  console.print("\n[bold]Colorado AI Act Impact Assessment[/bold]")
138
159
  console.print(
139
160
  "[dim]Answer each question honestly. IRIS uses your answers to generate "
140
- "a legally-meaningful impact assessment under SB 24-205.[/dim]\n"
161
+ "an impact assessment under SB 26-189 (replaces SB 24-205, effective "
162
+ "Jan. 1, 2027).[/dim]\n"
141
163
  )
142
164
 
143
165
  for i, q in enumerate(QUESTIONNAIRE, 1):
@@ -204,7 +226,28 @@ def calculate_risk_level(answers: dict) -> tuple:
204
226
  if domain in high_risk_domains:
205
227
  risk_score += 3
206
228
  findings.append(
207
- f"Agent operates in '{domain}' — a high-risk domain under SB 24-205."
229
+ f"Agent operates in '{domain}' — a covered ADMT domain under SB 26-189."
230
+ )
231
+
232
+ if not answers.get("impact_assessment"):
233
+ risk_score += 1
234
+ findings.append(
235
+ "No impact assessment on file (best practice under SB 26-189, "
236
+ "not legally required)."
237
+ )
238
+ recommendations.append(
239
+ "Complete an impact assessment via iris compliance assess to align "
240
+ "with NIST AI RMF MAP-1.5."
241
+ )
242
+
243
+ if not answers.get("record_retention"):
244
+ risk_score += 2
245
+ findings.append(
246
+ "No 3-year record retention plan documented for ADMT usage records."
247
+ )
248
+ recommendations.append(
249
+ "Configure Evidence Vault Pro for 3-year retention (CO-RR-001). "
250
+ "Run: iris license activate <your-key>"
208
251
  )
209
252
 
210
253
  if answers.get("consequential"):
@@ -297,8 +340,9 @@ def generate_assessment_markdown(
297
340
  > **Assessment ID**: `{assessment_id}`
298
341
  > **Assessed by**: {assessed_by}
299
342
  > **Date**: {now}
300
- > **Framework**: Colorado AI Act (SB 24-205), effective July 1, 2026
301
- > **Generated by**: IRIS Compliance Platform v0.1.0
343
+ > **Framework**: Colorado AI Act (SB 26-189), effective January 1, 2027
344
+ > **Note**: Replaces SB 24-205, signed May 14, 2026
345
+ > **Generated by**: IRIS Compliance Platform v{IRIS_PLATFORM_VERSION}
302
346
 
303
347
  ---
304
348
 
@@ -316,6 +360,8 @@ def generate_assessment_markdown(
316
360
  | Owner | {owner} |
317
361
  | Operating domain | {answers.get('domain', 'not specified')} |
318
362
  | Makes consequential decisions | {'Yes' if answers.get('consequential') else 'No'} |
363
+ | Impact assessment completed | {'Yes' if answers.get('impact_assessment') else 'No (best practice)'} |
364
+ | 3-year record retention plan | {'Yes' if answers.get('record_retention') else 'No'} |
319
365
  | Human review available | {'Yes' if answers.get('human_review') else 'No'} |
320
366
  | Consumer opt-out available | {'Yes' if answers.get('opt_out') else 'No'} |
321
367
  | Discrimination review completed | {'Yes' if answers.get('discrimination_review') else 'No'} |
@@ -356,12 +402,12 @@ def generate_assessment_markdown(
356
402
 
357
403
  | Rule | Obligation | Status |
358
404
  |---|---|---|
359
- | CO-001 | High-risk AI inventory | ✅ Satisfied — agent registered in IRIS |
360
- | CO-002 | Impact assessment | ✅ Satisfied this document |
361
- | CO-003 | Transparency disclosure | ✅ Satisfied — policy-intent.md |
362
- | CO-004 | Consumer opt-out | {'✅ Satisfied' if answers.get('opt_out') else '⚠️ Requires action'} |
363
- | CO-005 | Non-discrimination review | {'✅ Satisfied' if answers.get('discrimination_review') else '⚠️ Requires action'} |
364
- | CO-006 | Annual review | ✅ Schedulednext review due {(datetime.utcnow().replace(year=datetime.utcnow().year + 1)).strftime('%Y-%m-%d')} |
405
+ | CO-001 | ADMT inventory | ✅ Satisfied — agent registered in IRIS |
406
+ | CO-002 | Impact assessment (best practice) | {'✅ Satisfied' if answers.get('impact_assessment') else '⚠️ Recommended'} |
407
+ | CO-003 | Consumer notice / transparency | ✅ Satisfied — policy-intent.md |
408
+ | CO-004 | Post-adverse-action notice | {'✅ Satisfied' if answers.get('opt_out') else '⚠️ Requires action'} |
409
+ | CO-RR-001 | 3-year record retention | {'✅ Satisfied' if answers.get('record_retention') else '⚠️ Requires action'} |
410
+ | CO-DEV-001 | Developer documentation | ✅ Satisfiedpolicy-intent.md + passport.yaml |
365
411
 
366
412
  ---
367
413
 
@@ -374,9 +420,10 @@ def generate_assessment_markdown(
374
420
 
375
421
  ---
376
422
 
377
- *This document was generated by IRIS and constitutes a formal impact assessment
378
- under Colorado AI Act SB 24-205. It should be reviewed annually or whenever
379
- the agent's capabilities, data access, or decision scope changes.*
423
+ *This document was generated by IRIS and constitutes an impact assessment
424
+ under Colorado AI Act SB 26-189 (replaces SB 24-205, effective Jan. 1, 2027).
425
+ It should be reviewed whenever the agent's capabilities, data access, or
426
+ decision scope changes.*
380
427
  """
381
428
 
382
429
 
@@ -428,8 +475,8 @@ def compliance_assess(agent, assessor, governance_dir, yes, answers):
428
475
  console.print(Panel(
429
476
  f"[bold]IRIS Impact Assessment[/bold]\n"
430
477
  f"Agent: [cyan]{agent}[/cyan]\n"
431
- f"Framework: Colorado AI Act (SB 24-205)\n"
432
- f"Effective: July 1, 2026",
478
+ f"Framework: Colorado AI Act (SB 26-189)\n"
479
+ f"Effective: January 1, 2027 (replaces SB 24-205)",
433
480
  style="blue"
434
481
  ))
435
482
 
@@ -500,7 +547,7 @@ def compliance_assess(agent, assessor, governance_dir, yes, answers):
500
547
  f"Files updated:\n"
501
548
  f" [dim]{assessment_file}[/dim]\n"
502
549
  f" [dim]{passport_file}[/dim]\n\n"
503
- f"CO-002 violation: [bold green]CLOSED[/bold green]\n\n"
550
+ f"CO-002 best practice: [bold green]{'SATISFIED' if questionnaire_answers.get('impact_assessment') else 'RECOMMENDED'}[/bold green]\n\n"
504
551
  f"Next step: [bold]iris compliance check --framework colorado-ai-act[/bold]",
505
552
  style="green"
506
553
  ))
@@ -0,0 +1,127 @@
1
+ """iris compliance check — framework compliance with Pro preview."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+ from rich.console import Console
11
+
12
+ from iris import AgentPassport
13
+ from iris_core.compliance.framework_check import run_framework_check
14
+ from iris_core.compliance.results import ComplianceCheckStatus
15
+ from iris_core.entitlements.display import build_pro_preview_box
16
+
17
+ console = Console()
18
+
19
+
20
+ def _discover_passports(
21
+ gov_dir: Path,
22
+ agent: Optional[str],
23
+ ) -> list[AgentPassport]:
24
+ passports: list[AgentPassport] = []
25
+ if agent:
26
+ passport_file = gov_dir / agent / "passport.yaml"
27
+ if passport_file.exists():
28
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
29
+ return passports
30
+
31
+ for passport_file in gov_dir.rglob("passport.yaml"):
32
+ try:
33
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
34
+ except Exception:
35
+ continue
36
+ return passports
37
+
38
+
39
+ @click.command("check")
40
+ @click.option("--agent", default=None, help="Specific agent to check (or all)")
41
+ @click.option("--framework", "-f", default="colorado-ai-act")
42
+ @click.option("--dir", "governance_dir", type=click.Path(path_type=Path), default=None)
43
+ def compliance_check_cmd(
44
+ agent: Optional[str],
45
+ framework: str,
46
+ governance_dir: Optional[Path],
47
+ ) -> None:
48
+ """
49
+ Check an agent against a compliance framework.
50
+
51
+ Shows a detailed breakdown of which rules pass and fail,
52
+ with plain-English remediation guidance for each failure.
53
+
54
+ Example:
55
+ iris compliance check --framework colorado-ai-act
56
+ iris compliance check --agent payment-agent --framework hipaa
57
+ """
58
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
59
+ passports = _discover_passports(gov_dir, agent)
60
+ if not passports:
61
+ console.print("[yellow]No agent passports found.[/yellow]")
62
+ sys.exit(0)
63
+
64
+ all_pass = True
65
+ for passport in passports:
66
+ result = run_framework_check(passport, framework)
67
+
68
+ if result.preview_only:
69
+ preview_lines = [
70
+ f"{rule.rule_id} {rule.severity} {rule.name} [{rule.status}]"
71
+ for rule in result.rule_results
72
+ ]
73
+ console.print(
74
+ build_pro_preview_box(
75
+ framework,
76
+ result.framework_name,
77
+ result.total_controls,
78
+ preview_lines,
79
+ )
80
+ )
81
+ all_pass = False
82
+ continue
83
+
84
+ has_failures = any(rule.status == "FAIL" for rule in result.rule_results) or bool(
85
+ result.violations
86
+ )
87
+ status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
88
+ console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
89
+
90
+ if result.violations:
91
+ all_pass = False
92
+ for violation in result.violations:
93
+ console.print(f" [red]✗[/red] [{violation.rule_id}] {violation.message}")
94
+ if violation.remediation:
95
+ console.print(f" [yellow]→[/yellow] {violation.remediation}")
96
+
97
+ shown_ids = {v.rule_id for v in result.violations}
98
+ for rule in result.rule_results:
99
+ if rule.rule_id in shown_ids:
100
+ continue
101
+ if rule.status == "PASS":
102
+ console.print(f" [green]✓[/green] [{rule.rule_id}] {rule.name}")
103
+ elif rule.status == "FAIL":
104
+ all_pass = False
105
+ console.print(f" [red]✗[/red] [{rule.rule_id}] {rule.name}")
106
+ if rule.message:
107
+ console.print(f" {rule.message}")
108
+ if rule.remediation:
109
+ console.print(f" [yellow]→[/yellow] {rule.remediation}")
110
+
111
+ if not result.violations and not any(r.status == "FAIL" for r in result.rule_results):
112
+ console.print(f" [green]✓[/green] All {framework} rules satisfied")
113
+
114
+ for guidance in result.guidance:
115
+ if guidance.status == ComplianceCheckStatus.GUIDANCE:
116
+ console.print(f" [blue]ℹ[/blue] [{guidance.rule_id}] {guidance.message}")
117
+ if guidance.remediation:
118
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
119
+ elif guidance.status == ComplianceCheckStatus.PRO_REQUIRED:
120
+ console.print(f" [yellow]⚠[/yellow] [{guidance.rule_id}] {guidance.message}")
121
+ if guidance.remediation:
122
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
123
+
124
+ for note in result.check_notes:
125
+ console.print(f" [dim]ℹ {note}[/dim]")
126
+
127
+ sys.exit(0 if all_pass else 1)
@@ -13,7 +13,7 @@ import click
13
13
  from rich.console import Console
14
14
  from rich.table import Table
15
15
 
16
- from iris_core.compliance.license import IrisLicense
16
+ from iris_core.entitlements import Entitlements, Feature
17
17
  from iris_core.drift.detector import DriftDetector, DriftReport
18
18
  from iris_core.drift.notifier import DriftNotifier, load_alert_config, save_alert_config
19
19
 
@@ -24,15 +24,14 @@ def _gov_dir(governance_dir: Path | None) -> Path:
24
24
  return governance_dir or Path.cwd() / "governance" / "agents"
25
25
 
26
26
 
27
- def _require_pro_license(feature: str) -> None:
28
- result = IrisLicense().check("gdpr")
29
- if not result.valid:
30
- console.print(
31
- f"[red]{feature} requires an IRIS Pro license.[/red]\n"
32
- "Drift watch (terminal alerts) is available on the free tier.\n"
33
- "Run: iris license activate <your-key> | https://iris.ai/pricing"
34
- )
35
- sys.exit(1)
27
+ def _require_pro_license(feature_name: str, feature: Feature) -> None:
28
+ ents = Entitlements()
29
+ if not ents.has(feature):
30
+ try:
31
+ ents.require(feature, context=feature_name)
32
+ except Exception as exc:
33
+ console.print(str(exc))
34
+ sys.exit(1)
36
35
 
37
36
 
38
37
  def _render_check_table(report: DriftReport) -> None:
@@ -249,10 +248,10 @@ def drift_alert_config(slack_webhook: Optional[str], email: Optional[str], webho
249
248
  config = load_alert_config()
250
249
 
251
250
  if slack_webhook:
252
- _require_pro_license("Slack drift alerts")
251
+ _require_pro_license("Slack drift alerts", Feature.DRIFT_SLACK_ALERT)
253
252
  config["slack_webhook"] = slack_webhook
254
253
  if email:
255
- _require_pro_license("Email drift alerts")
254
+ _require_pro_license("Email drift alerts", Feature.DRIFT_EMAIL_ALERT)
256
255
  config["email"] = email
257
256
  if webhook:
258
257
  config["webhook"] = webhook
@@ -0,0 +1,16 @@
1
+ """iris entitlements — transparent feature map by tier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from iris_core.entitlements.display import build_entitlements_panel
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command("entitlements")
14
+ def entitlements_cmd() -> None:
15
+ """Show the complete IRIS feature map — what is available at each tier."""
16
+ console.print(build_entitlements_panel())
@@ -57,23 +57,25 @@ def _compliance_status(passport: AgentPassport) -> List[dict]:
57
57
  statuses: List[dict] = []
58
58
 
59
59
  checks = {
60
- "CO-001": passport.is_high_risk_ai and bool(passport.agent_id),
60
+ "CO-001": bool(passport.agent_id) and bool(passport.owner),
61
61
  "CO-002": bool(passport.evidence_vault_id),
62
62
  "CO-003": bool(passport.intent_ref),
63
63
  "CO-004": bool(passport.intent_ref),
64
- "CO-005": None,
65
- "CO-006": bool(passport.last_reviewed_at),
64
+ "CO-RR-001": None,
65
+ "CO-DEV-001": bool(passport.intent_ref) and bool(passport.description),
66
66
  }
67
67
 
68
68
  for rule in rules:
69
69
  rule_id = rule["rule_id"]
70
- if rule.get("phase") == 2:
70
+ if rule.get("status") == "BEST_PRACTICE":
71
+ passed = checks.get(rule_id)
72
+ status = "PASS" if passed else "RECOMMENDED"
71
73
  statuses.append(
72
74
  {
73
75
  "rule_id": rule_id,
74
76
  "name": rule["name"],
75
- "status": "PENDING",
76
- "detail": "Phase 2",
77
+ "status": status,
78
+ "detail": "Best practice (not legally required under SB 26-189)",
77
79
  }
78
80
  )
79
81
  continue
@@ -17,7 +17,32 @@ from rich.table import Table
17
17
  console = Console()
18
18
 
19
19
  MAJOR_CLOUDS = {"Google Cloud (GCP)", "AWS", "Microsoft Azure", "Multiple clouds"}
20
- FREE_FRAMEWORKS = {"colorado-ai-act"}
20
+ FREE_FRAMEWORKS = {
21
+ "colorado-ai-act",
22
+ "colorado-chatbot",
23
+ "colorado-health-ai",
24
+ "colorado-mental-health-ai",
25
+ }
26
+
27
+ COLORADO_AI_ACT_NOTE = (
28
+ "Note: Colorado SB 26-189 replaced the original Colorado AI Act on May 14, 2026. "
29
+ "Key changes: impact assessments are no longer legally required (retained as best "
30
+ "practice). Record retention for 3 years is now required. Effective January 1, 2027."
31
+ )
32
+
33
+ MENTAL_HEALTH_KEYWORDS = (
34
+ "therapy",
35
+ "counseling",
36
+ "mental health",
37
+ "psychotherapy",
38
+ "clinician",
39
+ )
40
+
41
+ CONVERSATIONAL_KEYWORDS = (
42
+ "conversational ai",
43
+ "chatbot",
44
+ "chat bot",
45
+ )
21
46
 
22
47
  Q1_CHOICES = [
23
48
  "Makes decisions that affect individual people (hiring, loans, medical, etc.)",
@@ -119,6 +144,10 @@ def _prefill_answers(passport: dict[str, Any] | None) -> dict[str, Any]:
119
144
  prefill["q6"] = True
120
145
  prefill["q2"] = Q2_CHOICES[0]
121
146
 
147
+ description = str(spec.get("description", "")).strip()
148
+ if description:
149
+ prefill["agent_description"] = description
150
+
122
151
  return prefill
123
152
 
124
153
 
@@ -205,11 +234,30 @@ def build_recommendations(answers: dict[str, Any]) -> list[Recommendation]:
205
234
  q6 = bool(answers.get("q6"))
206
235
  q7 = set(answers.get("q7", []))
207
236
  q8 = set(answers.get("q8", []))
237
+ agent_description = str(answers.get("agent_description", "")).lower()
208
238
 
209
239
  colorado = (
210
240
  (q5 and "Colorado" in q8)
211
241
  or (q1 == Q1_CHOICES[0] and "Colorado" in q8)
212
242
  )
243
+ conversational = (
244
+ any(kw in q1.lower() for kw in CONVERSATIONAL_KEYWORDS)
245
+ or any(kw in agent_description for kw in CONVERSATIONAL_KEYWORDS)
246
+ or q1 == Q1_CHOICES[3]
247
+ )
248
+ consumer_facing = q2 in {Q2_CHOICES[4], Q2_CHOICES[2]} or "minor" in agent_description
249
+ chatbot = conversational and consumer_facing and "Colorado" in q8
250
+
251
+ health_data = q4 == Q4_CHOICES[0] or "health" in str(q4).lower() or "medical" in str(q4).lower()
252
+ health_insurance = (
253
+ "HIPAA (healthcare)" in q7
254
+ or q2 == Q2_CHOICES[3]
255
+ or "health insurance" in agent_description
256
+ )
257
+ health_ai = (health_data or health_insurance) and "Colorado" in q8
258
+
259
+ mental_health = any(kw in agent_description for kw in MENTAL_HEALTH_KEYWORDS)
260
+
213
261
  nist = q6 or "FedRAMP (federal government contracts)" in q7 or q2 == Q2_CHOICES[0]
214
262
  fedramp = (
215
263
  (q6 or "FedRAMP (federal government contracts)" in q7 or q2 in {Q2_CHOICES[0], Q2_CHOICES[1]})
@@ -219,17 +267,61 @@ def build_recommendations(answers: dict[str, Any]) -> list[Recommendation]:
219
267
  soc2 = q2 == Q2_CHOICES[5] or "SOC 2 (SaaS / enterprise)" in q7
220
268
  gdpr = "Outside the US" in q8 or "Other / multiple states" in q8
221
269
 
270
+ colorado_reason = (
271
+ "Your agent makes consequential decisions for Colorado users. "
272
+ "SB 26-189 applies (replaces SB 24-205, effective Jan. 1, 2027). "
273
+ f"{COLORADO_AI_ACT_NOTE}"
274
+ if colorado
275
+ else "No Colorado consequential decision scope detected."
276
+ )
277
+
222
278
  recommendations = [
223
279
  Recommendation(
224
280
  framework="colorado-ai-act",
225
281
  tier="FREE",
226
282
  status="REQUIRED" if colorado else "NOT APPLICABLE",
283
+ reason=colorado_reason,
284
+ command="iris compliance check --framework colorado-ai-act" if colorado else None,
285
+ ),
286
+ Recommendation(
287
+ framework="colorado-chatbot",
288
+ tier="FREE",
289
+ status="REQUIRED" if chatbot else "NOT APPLICABLE",
227
290
  reason=(
228
- "Your agent makes consequential decisions for Colorado users. SB 24-205 applies."
229
- if colorado
230
- else "No Colorado consequential decision scope detected."
291
+ "Conversational AI serving Colorado consumers requires HB 26-1263 "
292
+ "chatbot safety controls (effective Jan. 1, 2027)."
293
+ if chatbot
294
+ else "No conversational AI + consumer audience trigger detected."
295
+ ),
296
+ command="iris compliance check --framework colorado-chatbot" if chatbot else None,
297
+ ),
298
+ Recommendation(
299
+ framework="colorado-health-ai",
300
+ tier="FREE",
301
+ status="REQUIRED" if health_ai else "NOT APPLICABLE",
302
+ reason=(
303
+ "Health or insurance data handling in Colorado requires HB 26-1139 "
304
+ "AI in health insurance controls (effective Jan. 1, 2027)."
305
+ if health_ai
306
+ else "No Colorado health insurance AI trigger detected."
307
+ ),
308
+ command="iris compliance check --framework colorado-health-ai" if health_ai else None,
309
+ ),
310
+ Recommendation(
311
+ framework="colorado-mental-health-ai",
312
+ tier="FREE",
313
+ status="REQUIRED" if mental_health else "NOT APPLICABLE",
314
+ reason=(
315
+ "Mental health services agent detected. HB 26-1195 applies "
316
+ "(effective Aug. 12, 2026 — two months from now)."
317
+ if mental_health
318
+ else "No mental health services scope detected."
319
+ ),
320
+ command=(
321
+ "iris compliance check --framework colorado-mental-health-ai"
322
+ if mental_health
323
+ else None
231
324
  ),
232
- command="iris compliance check --framework colorado-ai-act" if colorado else None,
233
325
  ),
234
326
  Recommendation(
235
327
  framework="nist-ai-rmf",
@@ -396,6 +488,8 @@ def framework_suggest(agent: str | None, output_format: str) -> None:
396
488
 
397
489
  prefill = _prefill_answers(passport)
398
490
  answers = run_questionnaire(prefill)
491
+ if prefill.get("agent_description"):
492
+ answers["agent_description"] = prefill["agent_description"]
399
493
  recommendations = build_recommendations(answers)
400
494
  save_path = _save_recommendations(agent, answers, recommendations)
401
495