iris-security-cli 0.1.7__tar.gz → 0.1.9__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_cli-0.1.7 → iris_security_cli-0.1.9}/PKG-INFO +5 -5
- iris_security_cli-0.1.9/iris_cli/certify.py +105 -0
- iris_security_cli-0.1.9/iris_cli/compliance_check_cmd.py +180 -0
- iris_security_cli-0.1.9/iris_cli/compliance_fix.py +202 -0
- iris_security_cli-0.1.9/iris_cli/declare.py +177 -0
- iris_security_cli-0.1.9/iris_cli/delegation.py +172 -0
- iris_security_cli-0.1.9/iris_cli/enforce.py +148 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/license_cmd.py +1 -1
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/main.py +119 -93
- iris_security_cli-0.1.9/iris_cli/models_cmd.py +138 -0
- iris_security_cli-0.1.9/iris_cli/preview.py +128 -0
- iris_security_cli-0.1.9/iris_cli/quickstart.py +423 -0
- iris_security_cli-0.1.9/iris_cli/sentinel.py +124 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/status_cmd.py +10 -0
- iris_security_cli-0.1.9/iris_cli/witness.py +123 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/PKG-INFO +5 -5
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/SOURCES.txt +14 -1
- iris_security_cli-0.1.9/iris_security_cli.egg-info/requires.txt +8 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/pyproject.toml +6 -5
- iris_security_cli-0.1.9/tests/test_mcp_model_governance.py +35 -0
- iris_security_cli-0.1.9/tests/test_quickstart.py +95 -0
- iris_security_cli-0.1.9/tests/test_vocabulary.py +353 -0
- iris_security_cli-0.1.7/iris_cli/compliance_check_cmd.py +0 -127
- iris_security_cli-0.1.7/iris_security_cli.egg-info/requires.txt +0 -8
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/README.md +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/assess.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/drift.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/entitlements_cmd.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/evidence.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/framework_suggest.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/framework_test.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/redteam.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/regulatory.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/scm.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/users.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/vault.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/setup.cfg +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/tests/test_entitlements_display.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/tests/test_framework_suggest.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/tests/test_framework_test.py +0 -0
- {iris_security_cli-0.1.7 → iris_security_cli-0.1.9}/tests/test_policy_diff.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris-security-cli
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: IRIS CLI —
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
7
7
|
Project-URL: Repository, https://github.com/gimartinb/iris-sdk
|
|
@@ -15,13 +15,13 @@ 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.
|
|
19
|
-
Requires-Dist: iris-security-sdk>=0.1.
|
|
18
|
+
Requires-Dist: iris-security-core>=0.1.7
|
|
19
|
+
Requires-Dist: iris-security-sdk>=0.1.9
|
|
20
20
|
Requires-Dist: click>=8.1
|
|
21
21
|
Requires-Dist: rich>=13.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
23
23
|
Provides-Extra: scm
|
|
24
|
-
Requires-Dist: iris-security-scm[all]>=0.1.
|
|
24
|
+
Requires-Dist: iris-security-scm[all]>=0.1.1; extra == "scm"
|
|
25
25
|
|
|
26
26
|
# iris-security-cli
|
|
27
27
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""iris certify — prove compliance readiness to any regulatory framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from iris_core.entitlements import Entitlements, Feature
|
|
14
|
+
from iris_core.models.passport import AgentPassport
|
|
15
|
+
|
|
16
|
+
from iris_cli.framework_test import (
|
|
17
|
+
_build_result,
|
|
18
|
+
_framework_name,
|
|
19
|
+
_load_previous_score,
|
|
20
|
+
_render_json,
|
|
21
|
+
_render_markdown,
|
|
22
|
+
_render_table,
|
|
23
|
+
_save_result,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_certification_header(
|
|
30
|
+
framework: str,
|
|
31
|
+
agent_name: str,
|
|
32
|
+
result,
|
|
33
|
+
) -> None:
|
|
34
|
+
framework_label = _framework_name(framework)
|
|
35
|
+
if framework == "colorado-ai-act":
|
|
36
|
+
framework_label = "Colorado AI Act (SB 26-189)"
|
|
37
|
+
|
|
38
|
+
console.print(Panel(
|
|
39
|
+
f"Agent: {agent_name}\n"
|
|
40
|
+
f"Framework: {framework_label}\n"
|
|
41
|
+
f"Generated: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}\n"
|
|
42
|
+
f"Attested: IRIS · Commit: local",
|
|
43
|
+
title=f"IRIS Certification — {framework}",
|
|
44
|
+
style="blue",
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
evaluated = result.passed_controls + result.failed_controls + result.partial_controls
|
|
48
|
+
pct = result.score_percent
|
|
49
|
+
filled = int((pct / 100) * 20)
|
|
50
|
+
bar = "█" * filled + "░" * (20 - filled)
|
|
51
|
+
readiness = "READY" if pct >= 95 else result.readiness_level
|
|
52
|
+
|
|
53
|
+
console.print(f"\n[bold]CERTIFICATION RESULT: {readiness}[/bold]")
|
|
54
|
+
console.print(f"{bar} {result.passed_controls} / {result.total_controls} controls {pct}%")
|
|
55
|
+
|
|
56
|
+
if framework == "colorado-ai-act" and pct >= 95:
|
|
57
|
+
console.print(
|
|
58
|
+
f"\nThis agent is certification-ready for Colorado AI Act (SB 26-189).\n"
|
|
59
|
+
f"Effective date: January 1, 2027\n\n"
|
|
60
|
+
f"Evidence package: iris evidence report --agent {agent_name}\n"
|
|
61
|
+
f"Auditor note: Present this output alongside the Evidence Vault report "
|
|
62
|
+
f"to demonstrate compliance readiness."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.command("certify")
|
|
67
|
+
@click.option(
|
|
68
|
+
"--framework",
|
|
69
|
+
required=True,
|
|
70
|
+
type=click.Choice(sorted(["colorado-ai-act", "nist-ai-rmf", "fedramp-moderate", "hipaa", "soc2", "gdpr"])),
|
|
71
|
+
)
|
|
72
|
+
@click.option("--agent", "agent_name", required=True, help="Agent name under governance/agents")
|
|
73
|
+
@click.option("--format", "output_format", default="table", type=click.Choice(["table", "json", "markdown"]))
|
|
74
|
+
def certify_cmd(framework: str, agent_name: str, output_format: str) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Certify compliance readiness against a regulatory framework.
|
|
77
|
+
|
|
78
|
+
Alias: iris test
|
|
79
|
+
"""
|
|
80
|
+
passport_path = Path.cwd() / "governance" / "agents" / agent_name / "passport.yaml"
|
|
81
|
+
if not passport_path.exists():
|
|
82
|
+
raise click.ClickException(f"Passport not found: {passport_path}")
|
|
83
|
+
passport = AgentPassport.from_yaml(passport_path.read_text())
|
|
84
|
+
|
|
85
|
+
ents = Entitlements()
|
|
86
|
+
has_pro = ents.has(Feature.CLI_TEST_FULL_REPORT)
|
|
87
|
+
|
|
88
|
+
result = _build_result(framework, agent_name, passport)
|
|
89
|
+
previous = _load_previous_score(agent_name, framework)
|
|
90
|
+
if previous is not None and has_pro:
|
|
91
|
+
result.progress_delta_percent = result.score_percent - previous
|
|
92
|
+
|
|
93
|
+
_save_result(result)
|
|
94
|
+
|
|
95
|
+
if output_format == "json":
|
|
96
|
+
click.echo(_render_json(result, has_pro))
|
|
97
|
+
elif output_format == "markdown":
|
|
98
|
+
click.echo(_render_markdown(result, has_pro))
|
|
99
|
+
else:
|
|
100
|
+
_render_certification_header(framework, agent_name, result)
|
|
101
|
+
_render_table(result, has_pro)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Legacy alias: iris test → iris certify
|
|
105
|
+
test = certify_cmd
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
from iris_cli.compliance_fix import offer_remediation, recheck_after_fixes
|
|
18
|
+
from iris_core.compliance.remediation import collect_fixable_issues
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _discover_passports(
|
|
24
|
+
gov_dir: Path,
|
|
25
|
+
agent: Optional[str],
|
|
26
|
+
) -> list[AgentPassport]:
|
|
27
|
+
passports: list[AgentPassport] = []
|
|
28
|
+
if agent:
|
|
29
|
+
passport_file = gov_dir / agent / "passport.yaml"
|
|
30
|
+
if passport_file.exists():
|
|
31
|
+
passports.append(AgentPassport.from_yaml(passport_file.read_text()))
|
|
32
|
+
return passports
|
|
33
|
+
|
|
34
|
+
for passport_file in gov_dir.rglob("passport.yaml"):
|
|
35
|
+
try:
|
|
36
|
+
passports.append(AgentPassport.from_yaml(passport_file.read_text()))
|
|
37
|
+
except Exception:
|
|
38
|
+
continue
|
|
39
|
+
return passports
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render_result(
|
|
43
|
+
passport: AgentPassport,
|
|
44
|
+
framework: str,
|
|
45
|
+
result,
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""Print one agent's check result. Returns False if failures were found."""
|
|
48
|
+
if result.preview_only:
|
|
49
|
+
preview_lines = [
|
|
50
|
+
f"{rule.rule_id} {rule.severity} {rule.name} [{rule.status}]"
|
|
51
|
+
for rule in result.rule_results
|
|
52
|
+
]
|
|
53
|
+
console.print(
|
|
54
|
+
build_pro_preview_box(
|
|
55
|
+
framework,
|
|
56
|
+
result.framework_name,
|
|
57
|
+
result.total_controls,
|
|
58
|
+
preview_lines,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
has_failures = any(rule.status == "FAIL" for rule in result.rule_results) or bool(
|
|
64
|
+
result.violations
|
|
65
|
+
)
|
|
66
|
+
status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
|
|
67
|
+
console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
|
|
68
|
+
|
|
69
|
+
if result.violations:
|
|
70
|
+
for violation in result.violations:
|
|
71
|
+
console.print(f" [red]✗[/red] [{violation.rule_id}] {violation.message}")
|
|
72
|
+
if violation.remediation:
|
|
73
|
+
console.print(f" [yellow]→[/yellow] {violation.remediation}")
|
|
74
|
+
|
|
75
|
+
shown_ids = {v.rule_id for v in result.violations}
|
|
76
|
+
for rule in result.rule_results:
|
|
77
|
+
if rule.rule_id in shown_ids:
|
|
78
|
+
continue
|
|
79
|
+
if rule.status == "PASS":
|
|
80
|
+
console.print(f" [green]✓[/green] [{rule.rule_id}] {rule.name}")
|
|
81
|
+
elif rule.status == "FAIL":
|
|
82
|
+
console.print(f" [red]✗[/red] [{rule.rule_id}] {rule.name}")
|
|
83
|
+
if rule.message:
|
|
84
|
+
console.print(f" {rule.message}")
|
|
85
|
+
if rule.remediation:
|
|
86
|
+
console.print(f" [yellow]→[/yellow] {rule.remediation}")
|
|
87
|
+
|
|
88
|
+
if not result.violations and not any(r.status == "FAIL" for r in result.rule_results):
|
|
89
|
+
console.print(f" [green]✓[/green] All {framework} rules satisfied")
|
|
90
|
+
|
|
91
|
+
for guidance in result.guidance:
|
|
92
|
+
if guidance.status == ComplianceCheckStatus.GUIDANCE:
|
|
93
|
+
console.print(f" [blue]ℹ[/blue] [{guidance.rule_id}] {guidance.message}")
|
|
94
|
+
if guidance.remediation:
|
|
95
|
+
console.print(f" [yellow]→[/yellow] {guidance.remediation}")
|
|
96
|
+
elif guidance.status == ComplianceCheckStatus.PRO_REQUIRED:
|
|
97
|
+
console.print(f" [yellow]⚠[/yellow] [{guidance.rule_id}] {guidance.message}")
|
|
98
|
+
if guidance.remediation:
|
|
99
|
+
console.print(f" [yellow]→[/yellow] {guidance.remediation}")
|
|
100
|
+
|
|
101
|
+
for note in result.check_notes:
|
|
102
|
+
console.print(f" [dim]ℹ {note}[/dim]")
|
|
103
|
+
|
|
104
|
+
return not has_failures
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.command("check")
|
|
108
|
+
@click.option("--agent", default=None, help="Specific agent to check (or all)")
|
|
109
|
+
@click.option("--framework", "-f", default="colorado-ai-act")
|
|
110
|
+
@click.option("--dir", "governance_dir", type=click.Path(path_type=Path), default=None)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--fix",
|
|
113
|
+
is_flag=True,
|
|
114
|
+
help="Apply all auto-fixable remediations without per-issue prompts",
|
|
115
|
+
)
|
|
116
|
+
@click.option(
|
|
117
|
+
"--yes",
|
|
118
|
+
"-y",
|
|
119
|
+
is_flag=True,
|
|
120
|
+
help="Apply auto-fixes without confirmation (use with --fix)",
|
|
121
|
+
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--no-fix",
|
|
124
|
+
is_flag=True,
|
|
125
|
+
help="Show remediations only — never offer to apply fixes",
|
|
126
|
+
)
|
|
127
|
+
def compliance_check_cmd(
|
|
128
|
+
agent: Optional[str],
|
|
129
|
+
framework: str,
|
|
130
|
+
governance_dir: Optional[Path],
|
|
131
|
+
fix: bool,
|
|
132
|
+
yes: bool,
|
|
133
|
+
no_fix: bool,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Check an agent against a compliance framework.
|
|
137
|
+
|
|
138
|
+
Shows a detailed breakdown of which rules pass and fail,
|
|
139
|
+
with plain-English remediation guidance for each failure.
|
|
140
|
+
When run interactively, IRIS offers to apply safe fixes automatically.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
iris compliance check --framework colorado-ai-act
|
|
144
|
+
iris compliance check --agent payment-agent --framework hipaa
|
|
145
|
+
iris compliance check --agent payment-agent --framework hipaa --fix -y
|
|
146
|
+
"""
|
|
147
|
+
gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
|
|
148
|
+
passports = _discover_passports(gov_dir, agent)
|
|
149
|
+
if not passports:
|
|
150
|
+
console.print("[yellow]No agent passports found.[/yellow]")
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
all_pass = True
|
|
154
|
+
offer_fixes = bool(agent) or len(passports) == 1
|
|
155
|
+
for passport in passports:
|
|
156
|
+
result = run_framework_check(passport, framework)
|
|
157
|
+
agent_passed = _render_result(passport, framework, result)
|
|
158
|
+
if not agent_passed:
|
|
159
|
+
all_pass = False
|
|
160
|
+
|
|
161
|
+
if offer_fixes:
|
|
162
|
+
applied = offer_remediation(
|
|
163
|
+
passport,
|
|
164
|
+
framework,
|
|
165
|
+
result,
|
|
166
|
+
gov_dir,
|
|
167
|
+
auto_fix=fix,
|
|
168
|
+
yes=yes,
|
|
169
|
+
no_fix=no_fix,
|
|
170
|
+
)
|
|
171
|
+
recheck_after_fixes(passport, framework, gov_dir, applied)
|
|
172
|
+
elif not no_fix and not agent_passed and collect_fixable_issues(
|
|
173
|
+
passport, framework, result
|
|
174
|
+
):
|
|
175
|
+
console.print(
|
|
176
|
+
f"[dim]Fixable issues for {passport.name}. "
|
|
177
|
+
f"Re-run with --agent {passport.name} to let IRIS apply fixes.[/dim]"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
sys.exit(0 if all_pass else 1)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Interactive compliance remediation — IRIS governance assistant."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from iris import AgentPassport
|
|
13
|
+
from iris_core.compliance.framework_check import FrameworkCheckResult, run_framework_check
|
|
14
|
+
from iris_core.compliance.remediation import (
|
|
15
|
+
FixResult,
|
|
16
|
+
FixableIssue,
|
|
17
|
+
apply_fix,
|
|
18
|
+
collect_fixable_issues,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_interactive() -> bool:
|
|
25
|
+
return click.get_text_stream("stdin").isatty()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _print_fixable_summary(agent_name: str, issues: List[FixableIssue]) -> None:
|
|
29
|
+
lines = [
|
|
30
|
+
f"[bold]IRIS found {len(issues)} fixable issue"
|
|
31
|
+
f"{'s' if len(issues) != 1 else ''} for [cyan]{agent_name}[/cyan][/bold]\n"
|
|
32
|
+
]
|
|
33
|
+
for issue in issues:
|
|
34
|
+
lines.append(f" [yellow]•[/yellow] [{issue.rule_id}] {issue.preview}")
|
|
35
|
+
lines.append(
|
|
36
|
+
"\n[dim]IRIS can apply safe passport and policy updates automatically. "
|
|
37
|
+
"Review each change before applying.[/dim]"
|
|
38
|
+
)
|
|
39
|
+
console.print(Panel("\n".join(lines), title="IRIS Governance Assistant", style="blue"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def offer_remediation(
|
|
43
|
+
passport: AgentPassport,
|
|
44
|
+
framework: str,
|
|
45
|
+
result: FrameworkCheckResult,
|
|
46
|
+
governance_dir: Path,
|
|
47
|
+
*,
|
|
48
|
+
auto_fix: bool = False,
|
|
49
|
+
yes: bool = False,
|
|
50
|
+
no_fix: bool = False,
|
|
51
|
+
) -> List[FixResult]:
|
|
52
|
+
"""
|
|
53
|
+
Offer to apply fixable remediations after a compliance check.
|
|
54
|
+
|
|
55
|
+
Returns applied fix results (may be empty).
|
|
56
|
+
"""
|
|
57
|
+
if no_fix or result.preview_only:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
issues = collect_fixable_issues(passport, framework, result)
|
|
61
|
+
if not issues:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
agent_dir = governance_dir / passport.name
|
|
65
|
+
passport_file = agent_dir / "passport.yaml"
|
|
66
|
+
|
|
67
|
+
if auto_fix or yes:
|
|
68
|
+
return _apply_all(issues, passport_file, framework, passport.name)
|
|
69
|
+
|
|
70
|
+
if not _is_interactive():
|
|
71
|
+
console.print(
|
|
72
|
+
"[dim]Fixable issues detected. Re-run with --fix to apply automatically.[/dim]"
|
|
73
|
+
)
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
_print_fixable_summary(passport.name, issues)
|
|
77
|
+
if not click.confirm(
|
|
78
|
+
f"\nWould you like IRIS to fix these {len(issues)} issue"
|
|
79
|
+
f"{'s' if len(issues) != 1 else ''} for {passport.name}?",
|
|
80
|
+
default=True,
|
|
81
|
+
):
|
|
82
|
+
console.print("[dim]Skipped automatic fixes. Apply remediations manually when ready.[/dim]")
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
return _apply_interactive(issues, passport_file, framework, passport.name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _apply_all(
|
|
89
|
+
issues: List[FixableIssue],
|
|
90
|
+
passport_file: Path,
|
|
91
|
+
framework: str,
|
|
92
|
+
agent_name: str,
|
|
93
|
+
) -> List[FixResult]:
|
|
94
|
+
results: List[FixResult] = []
|
|
95
|
+
for issue in issues:
|
|
96
|
+
result = apply_fix(issue, passport_file, framework)
|
|
97
|
+
results.append(result)
|
|
98
|
+
_print_fix_result(agent_name, issue, result)
|
|
99
|
+
if results:
|
|
100
|
+
console.print(
|
|
101
|
+
Panel(
|
|
102
|
+
f"[bold green]✓ IRIS applied {sum(1 for r in results if r.applied)} fix"
|
|
103
|
+
f"{'es' if sum(1 for r in results if r.applied) != 1 else ''}[/bold green]\n\n"
|
|
104
|
+
f"Re-run: [bold]iris compliance check --agent {agent_name} "
|
|
105
|
+
f"--framework {framework}[/bold]",
|
|
106
|
+
style="green",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
return results
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _apply_interactive(
|
|
113
|
+
issues: List[FixableIssue],
|
|
114
|
+
passport_file: Path,
|
|
115
|
+
framework: str,
|
|
116
|
+
agent_name: str,
|
|
117
|
+
) -> List[FixResult]:
|
|
118
|
+
results: List[FixResult] = []
|
|
119
|
+
apply_all = False
|
|
120
|
+
|
|
121
|
+
for issue in issues:
|
|
122
|
+
if apply_all:
|
|
123
|
+
result = apply_fix(issue, passport_file, framework)
|
|
124
|
+
results.append(result)
|
|
125
|
+
_print_fix_result(agent_name, issue, result)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
f"\n[bold]IRIS can fix[/bold] [{issue.rule_id}] {issue.preview}\n"
|
|
130
|
+
f"[dim]{issue.message}[/dim]"
|
|
131
|
+
)
|
|
132
|
+
console.print("Apply this fix? ([yes]/no/skip/all)")
|
|
133
|
+
choice = click.prompt("", default="no", show_default=False).strip().lower()
|
|
134
|
+
|
|
135
|
+
if choice in ("yes", "y"):
|
|
136
|
+
result = apply_fix(issue, passport_file, framework)
|
|
137
|
+
results.append(result)
|
|
138
|
+
_print_fix_result(agent_name, issue, result)
|
|
139
|
+
elif choice == "all":
|
|
140
|
+
apply_all = True
|
|
141
|
+
result = apply_fix(issue, passport_file, framework)
|
|
142
|
+
results.append(result)
|
|
143
|
+
_print_fix_result(agent_name, issue, result)
|
|
144
|
+
elif choice == "skip":
|
|
145
|
+
console.print("[dim]Skipped.[/dim]")
|
|
146
|
+
else:
|
|
147
|
+
console.print("[dim]Not applied — fix manually when ready.[/dim]")
|
|
148
|
+
|
|
149
|
+
applied_count = sum(1 for r in results if r.applied)
|
|
150
|
+
if applied_count:
|
|
151
|
+
console.print(
|
|
152
|
+
Panel(
|
|
153
|
+
f"[bold green]✓ IRIS applied {applied_count} fix"
|
|
154
|
+
f"{'es' if applied_count != 1 else ''}[/bold green]\n\n"
|
|
155
|
+
f"Re-run: [bold]iris compliance check --agent {agent_name} "
|
|
156
|
+
f"--framework {framework}[/bold]",
|
|
157
|
+
style="green",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _print_fix_result(agent_name: str, issue: FixableIssue, result: FixResult) -> None:
|
|
164
|
+
if result.applied:
|
|
165
|
+
change_text = "\n".join(f" [dim]• {change}[/dim]" for change in result.changes)
|
|
166
|
+
console.print(f"[green]✓ Applied[/green] [{issue.rule_id}]\n{change_text}")
|
|
167
|
+
return
|
|
168
|
+
if result.skipped_reason:
|
|
169
|
+
console.print(
|
|
170
|
+
f"[yellow]⚠ Skipped[/yellow] [{issue.rule_id}]: {result.skipped_reason}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def recheck_after_fixes(
|
|
175
|
+
passport: AgentPassport,
|
|
176
|
+
framework: str,
|
|
177
|
+
governance_dir: Path,
|
|
178
|
+
applied: List[FixResult],
|
|
179
|
+
) -> Optional[FrameworkCheckResult]:
|
|
180
|
+
"""Re-run check and show delta when fixes were applied."""
|
|
181
|
+
if not any(result.applied for result in applied):
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
passport_file = governance_dir / passport.name / "passport.yaml"
|
|
185
|
+
if not passport_file.exists():
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
updated = AgentPassport.from_yaml(passport_file.read_text())
|
|
189
|
+
result = run_framework_check(updated, framework)
|
|
190
|
+
remaining = collect_fixable_issues(updated, framework, result)
|
|
191
|
+
fail_count = sum(1 for rule in result.rule_results if rule.status == "FAIL")
|
|
192
|
+
fail_count += len(result.violations)
|
|
193
|
+
|
|
194
|
+
console.print(
|
|
195
|
+
Panel(
|
|
196
|
+
f"[bold]Re-check: {updated.name}[/bold]\n"
|
|
197
|
+
f"Remaining failures: {fail_count}\n"
|
|
198
|
+
f"Auto-fixable remaining: {len(remaining)}",
|
|
199
|
+
style="cyan",
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return result
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""iris declare — register an agent with optional interactive wizard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
_DATA_CLASSIFICATIONS = {
|
|
15
|
+
"none": "internal",
|
|
16
|
+
"internal": "internal",
|
|
17
|
+
"pii": "pii",
|
|
18
|
+
"phi": "phi",
|
|
19
|
+
"financial": "confidential",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _run_framework_suggest(agent_name: str) -> None:
|
|
24
|
+
console.print(f"[dim]Run: iris framework suggest --agent {agent_name}[/dim]")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write_agent_files(
|
|
28
|
+
name: str,
|
|
29
|
+
owner: str,
|
|
30
|
+
team: str,
|
|
31
|
+
env: tuple[str, ...],
|
|
32
|
+
high_risk: bool,
|
|
33
|
+
compliance_list: list[str],
|
|
34
|
+
output: Path,
|
|
35
|
+
description: str = "",
|
|
36
|
+
) -> None:
|
|
37
|
+
from iris import IrisAgent, DataClassification, ComplianceTag
|
|
38
|
+
|
|
39
|
+
data_class = DataClassification.INTERNAL
|
|
40
|
+
agent = IrisAgent(
|
|
41
|
+
name=name,
|
|
42
|
+
owner=owner,
|
|
43
|
+
team=team,
|
|
44
|
+
compliance=compliance_list,
|
|
45
|
+
environments=list(env),
|
|
46
|
+
is_high_risk_ai=high_risk,
|
|
47
|
+
policy_dir=output,
|
|
48
|
+
)
|
|
49
|
+
if description:
|
|
50
|
+
agent.passport.description = description
|
|
51
|
+
|
|
52
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
(output / "passport.yaml").write_text(agent.passport.to_yaml())
|
|
54
|
+
|
|
55
|
+
intent_template = f"""# Policy Intent — {name}
|
|
56
|
+
|
|
57
|
+
> Edit this file to describe what your agent is allowed to do.
|
|
58
|
+
> Then run: iris compile --agent {name}
|
|
59
|
+
|
|
60
|
+
## What this agent does
|
|
61
|
+
{description or "[Describe the agent's purpose here]"}
|
|
62
|
+
|
|
63
|
+
## What it is allowed to access
|
|
64
|
+
[List the tools, APIs, and data sources this agent needs]
|
|
65
|
+
|
|
66
|
+
## What it must never do
|
|
67
|
+
[List explicit prohibitions]
|
|
68
|
+
|
|
69
|
+
## Compliance notes
|
|
70
|
+
[Any compliance-specific context]
|
|
71
|
+
"""
|
|
72
|
+
(output / "policy-intent.md").write_text(intent_template)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _interactive_wizard(output_dir: Optional[Path]) -> None:
|
|
76
|
+
console.print(Panel("[bold]IRIS Agent Declaration[/bold]", style="blue"))
|
|
77
|
+
|
|
78
|
+
name = click.prompt("What is this agent's name?", type=str).strip()
|
|
79
|
+
description = click.prompt("What does this agent do? (plain English)", type=str).strip()
|
|
80
|
+
owner = click.prompt("Who owns it? (email)", type=str).strip()
|
|
81
|
+
team = click.prompt("Which team?", type=str).strip()
|
|
82
|
+
high_risk = click.confirm("Does it make consequential decisions?", default=False)
|
|
83
|
+
|
|
84
|
+
console.print("What data does it access?")
|
|
85
|
+
console.print(" 1. None 2. Internal only 3. PII 4. PHI 5. Financial")
|
|
86
|
+
data_choice = click.prompt("Choice", type=click.IntRange(1, 5), default=2)
|
|
87
|
+
data_keys = ["none", "internal", "pii", "phi", "financial"]
|
|
88
|
+
data_key = data_keys[data_choice - 1]
|
|
89
|
+
|
|
90
|
+
output = output_dir or (Path.cwd() / "governance" / "agents" / name)
|
|
91
|
+
compliance_list = ["colorado-ai-act"]
|
|
92
|
+
|
|
93
|
+
if not click.confirm(f"Write agent files to {output}?", default=True):
|
|
94
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
_write_agent_files(
|
|
98
|
+
name=name,
|
|
99
|
+
owner=owner,
|
|
100
|
+
team=team,
|
|
101
|
+
env=("dev",),
|
|
102
|
+
high_risk=high_risk,
|
|
103
|
+
compliance_list=compliance_list,
|
|
104
|
+
output=output,
|
|
105
|
+
description=description,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
console.print("\n[bold]Recommended compliance tags:[/bold]")
|
|
109
|
+
_run_framework_suggest(name)
|
|
110
|
+
|
|
111
|
+
console.print(Panel(
|
|
112
|
+
f"[bold green]✓ Agent declared[/bold green]\n\n"
|
|
113
|
+
f"Name: [cyan]{name}[/cyan]\n"
|
|
114
|
+
f"Data: {_DATA_CLASSIFICATIONS.get(data_key, 'internal')}\n"
|
|
115
|
+
f"Passport: {output / 'passport.yaml'}\n\n"
|
|
116
|
+
f"Next: Edit [bold]{output / 'policy-intent.md'}[/bold]\n"
|
|
117
|
+
f"Then: [bold]iris compile --agent {name}[/bold]",
|
|
118
|
+
style="green",
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@click.command("declare")
|
|
123
|
+
@click.option("--name", default=None, help="Agent identifier (kebab-case)")
|
|
124
|
+
@click.option("--owner", default=None, help="Owner email address")
|
|
125
|
+
@click.option("--team", default=None, help="Team or squad name")
|
|
126
|
+
@click.option("--env", multiple=True, default=None, help="Environments (repeatable)")
|
|
127
|
+
@click.option("--high-risk", is_flag=True, default=False, help="Flag as high-risk AI")
|
|
128
|
+
@click.option("--compliance", "-c", multiple=True, help="Compliance frameworks to tag")
|
|
129
|
+
@click.option("--dir", "output_dir", type=click.Path(path_type=Path), default=None)
|
|
130
|
+
def declare(
|
|
131
|
+
name: Optional[str],
|
|
132
|
+
owner: Optional[str],
|
|
133
|
+
team: Optional[str],
|
|
134
|
+
env: Optional[tuple[str, ...]],
|
|
135
|
+
high_risk: bool,
|
|
136
|
+
compliance: tuple[str, ...],
|
|
137
|
+
output_dir: Optional[Path],
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Declare what an agent is allowed to do — creates passport.yaml and intent template.
|
|
141
|
+
|
|
142
|
+
Run without flags for the interactive declaration wizard.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
iris declare
|
|
146
|
+
iris declare --name payment-agent --owner alice@co.com --team platform
|
|
147
|
+
"""
|
|
148
|
+
if name is None and owner is None and team is None:
|
|
149
|
+
_interactive_wizard(output_dir)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
resolved_name = name or click.prompt("Agent name")
|
|
153
|
+
resolved_owner = owner or click.prompt("Owner email")
|
|
154
|
+
resolved_team = team or click.prompt("Team name")
|
|
155
|
+
compliance_list = list(compliance) or ["colorado-ai-act"]
|
|
156
|
+
env_list = tuple(env) if env else ("dev",)
|
|
157
|
+
output = output_dir or (Path.cwd() / "governance" / "agents" / resolved_name)
|
|
158
|
+
|
|
159
|
+
_write_agent_files(
|
|
160
|
+
name=resolved_name,
|
|
161
|
+
owner=resolved_owner,
|
|
162
|
+
team=resolved_team,
|
|
163
|
+
env=env_list,
|
|
164
|
+
high_risk=high_risk,
|
|
165
|
+
compliance_list=compliance_list,
|
|
166
|
+
output=output,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
console.print(Panel(
|
|
170
|
+
f"[bold green]✓ Agent declared[/bold green]\n\n"
|
|
171
|
+
f"Name: [cyan]{resolved_name}[/cyan]\n"
|
|
172
|
+
f"Passport: {output / 'passport.yaml'}\n"
|
|
173
|
+
f"Intent template: {output / 'policy-intent.md'}\n\n"
|
|
174
|
+
f"Next step: Edit [bold]{output / 'policy-intent.md'}[/bold]\n"
|
|
175
|
+
f"Then run: [bold]iris compile --agent {resolved_name}[/bold]",
|
|
176
|
+
style="green",
|
|
177
|
+
))
|