iris-security-cli 0.1.0__py3-none-any.whl
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_cli/__init__.py +0 -0
- iris_cli/assess.py +498 -0
- iris_cli/cedar_parser.py +454 -0
- iris_cli/compiler_config.py +54 -0
- iris_cli/evidence.py +822 -0
- iris_cli/main.py +542 -0
- iris_cli/mcp_server.py +567 -0
- iris_cli/policy_cache.py +116 -0
- iris_cli/policy_diff.py +467 -0
- iris_cli/scan_report.py +146 -0
- iris_security_cli-0.1.0.dist-info/METADATA +45 -0
- iris_security_cli-0.1.0.dist-info/RECORD +17 -0
- iris_security_cli-0.1.0.dist-info/WHEEL +5 -0
- iris_security_cli-0.1.0.dist-info/entry_points.txt +2 -0
- iris_security_cli-0.1.0.dist-info/top_level.txt +2 -0
- tests/test_evidence.py +296 -0
- tests/test_policy_diff.py +250 -0
iris_cli/main.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IRIS CLI — the developer's command-line interface for agent governance.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
iris scan — scan governance directory for policy violations
|
|
6
|
+
iris policy generate — generate Cedar policy from intent file
|
|
7
|
+
iris policy compile — compile natural language to Cedar
|
|
8
|
+
iris policy diff — preview Cedar changes offline (no API cost)
|
|
9
|
+
iris policy check — validate policy against compliance framework
|
|
10
|
+
iris register — register a new agent (creates passport.yaml)
|
|
11
|
+
iris compliance check — run compliance check against a framework
|
|
12
|
+
iris compliance report — generate a compliance report (PDF/markdown)
|
|
13
|
+
|
|
14
|
+
This CLI is the traction instrument. Every 'iris scan' or 'iris policy check'
|
|
15
|
+
is a weekly active developer event.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group()
|
|
28
|
+
@click.version_option(version="0.1.0", prog_name="iris")
|
|
29
|
+
def cli():
|
|
30
|
+
"""
|
|
31
|
+
IRIS — AI Agent Governance Platform
|
|
32
|
+
|
|
33
|
+
Discover, register, and govern AI agents locally.
|
|
34
|
+
Colorado AI Act compliant out of the box.
|
|
35
|
+
|
|
36
|
+
Docs: https://docs.iris.ai
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── iris scan ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@cli.command()
|
|
44
|
+
@click.option("--dir", "scan_dir", type=Path, default=Path.cwd(), help="Directory to scan")
|
|
45
|
+
@click.option("--framework", "-f", default=None, help="Compliance framework (e.g. colorado-ai-act)")
|
|
46
|
+
@click.option("--format", "output_format", default="table", type=click.Choice(["table", "json", "markdown"]))
|
|
47
|
+
@click.option("--fail-on", default="critical", type=click.Choice(["critical", "high", "any"]))
|
|
48
|
+
@click.option(
|
|
49
|
+
"--discover",
|
|
50
|
+
is_flag=True,
|
|
51
|
+
help="Scan Python/TypeScript source files for ungoverned AI agent patterns",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--auto-register",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
help="Write passport.yaml drafts for each ungoverned finding (does not register)",
|
|
57
|
+
)
|
|
58
|
+
def scan(scan_dir: Path, framework: str, output_format: str, fail_on: str, discover: bool, auto_register: bool):
|
|
59
|
+
"""
|
|
60
|
+
Scan governance directory for policy violations.
|
|
61
|
+
|
|
62
|
+
Finds all passport.yaml files and checks them against the
|
|
63
|
+
configured compliance frameworks. Exits with code 1 if violations
|
|
64
|
+
above the --fail-on threshold are found (for CI integration).
|
|
65
|
+
|
|
66
|
+
With --discover, also crawls Python and TypeScript files for ungoverned
|
|
67
|
+
LLM/agent patterns (LangChain, OpenAI, Anthropic, CrewAI, etc.).
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
iris scan
|
|
71
|
+
iris scan --framework colorado-ai-act
|
|
72
|
+
iris scan --discover
|
|
73
|
+
iris scan --discover --auto-register
|
|
74
|
+
iris scan --format json | jq '.violations[]'
|
|
75
|
+
"""
|
|
76
|
+
from iris import iris_scan
|
|
77
|
+
from iris_core.models.policy import Severity
|
|
78
|
+
|
|
79
|
+
if discover:
|
|
80
|
+
from iris_core.discovery.scanner import CodebaseScanner
|
|
81
|
+
from iris_core.models.passport import AgentPassport, ComplianceTag, DataClassification
|
|
82
|
+
from iris_cli.scan_report import render_discover_scan
|
|
83
|
+
|
|
84
|
+
scanner = CodebaseScanner()
|
|
85
|
+
discover_result = scanner.scan_directory(scan_dir)
|
|
86
|
+
drafts_written = []
|
|
87
|
+
|
|
88
|
+
if auto_register:
|
|
89
|
+
gov_root = scan_dir / "governance" / "agents"
|
|
90
|
+
for finding in discover_result.ungoverned_findings:
|
|
91
|
+
agent_dir = gov_root / finding.agent_name_hint
|
|
92
|
+
passport_path = agent_dir / "passport.yaml"
|
|
93
|
+
if passport_path.exists():
|
|
94
|
+
continue
|
|
95
|
+
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
draft = AgentPassport(
|
|
97
|
+
name=finding.agent_name_hint,
|
|
98
|
+
owner="CHANGE_ME@company.com",
|
|
99
|
+
team="CHANGE_ME",
|
|
100
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
101
|
+
data_classification=DataClassification.INTERNAL,
|
|
102
|
+
is_high_risk_ai="--high-risk" in finding.suggested_command,
|
|
103
|
+
description=(
|
|
104
|
+
f"Auto-drafted from ungoverned scan finding in "
|
|
105
|
+
f"{finding.file_path}:{finding.line_number}"
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
passport_path.write_text(draft.to_yaml())
|
|
109
|
+
drafts_written.append(passport_path)
|
|
110
|
+
|
|
111
|
+
if output_format == "json":
|
|
112
|
+
import json
|
|
113
|
+
|
|
114
|
+
payload = {
|
|
115
|
+
"scan_timestamp": discover_result.scan_timestamp,
|
|
116
|
+
"files_scanned": discover_result.files_scanned,
|
|
117
|
+
"lines_scanned": discover_result.lines_scanned,
|
|
118
|
+
"governed_agents": [p.name for p in discover_result.governed_agents],
|
|
119
|
+
"ungoverned_findings": [
|
|
120
|
+
{
|
|
121
|
+
"file_path": f.file_path,
|
|
122
|
+
"line_number": f.line_number,
|
|
123
|
+
"pattern_matched": f.pattern_matched,
|
|
124
|
+
"framework_detected": f.framework_detected,
|
|
125
|
+
"agent_name_hint": f.agent_name_hint,
|
|
126
|
+
"suggested_command": f.suggested_command,
|
|
127
|
+
"risk_level": f.risk_level,
|
|
128
|
+
"risk_reason": f.risk_reason,
|
|
129
|
+
}
|
|
130
|
+
for f in discover_result.ungoverned_findings
|
|
131
|
+
],
|
|
132
|
+
"shadow_agents": [
|
|
133
|
+
{
|
|
134
|
+
"resource_path": s.resource_path,
|
|
135
|
+
"resource_name": s.resource_name,
|
|
136
|
+
"namespace": s.namespace,
|
|
137
|
+
"reason": s.reason,
|
|
138
|
+
}
|
|
139
|
+
for s in discover_result.shadow_agents
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
click.echo(json.dumps(payload, indent=2))
|
|
143
|
+
else:
|
|
144
|
+
render_discover_scan(
|
|
145
|
+
console,
|
|
146
|
+
discover_result,
|
|
147
|
+
scan_dir,
|
|
148
|
+
framework=framework,
|
|
149
|
+
auto_register=auto_register,
|
|
150
|
+
drafts_written=drafts_written,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if discover_result.ungoverned_findings:
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
|
|
157
|
+
console.print(Panel(
|
|
158
|
+
f"[bold]IRIS Governance Scan[/bold]\n"
|
|
159
|
+
f"Directory: {scan_dir}\n"
|
|
160
|
+
f"Framework: {framework or 'all active bundles'}",
|
|
161
|
+
style="blue"
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
violations = iris_scan(directory=scan_dir, framework=framework)
|
|
165
|
+
|
|
166
|
+
if not violations:
|
|
167
|
+
console.print("\n[bold green]✓ All agents passed compliance check[/bold green]")
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
|
|
170
|
+
if output_format == "table":
|
|
171
|
+
from iris_cli.scan_report import render_violations_table
|
|
172
|
+
|
|
173
|
+
render_violations_table(console, violations)
|
|
174
|
+
|
|
175
|
+
elif output_format == "json":
|
|
176
|
+
import json
|
|
177
|
+
output = [{"rule_id": v.rule_id, "severity": v.severity.value, "message": v.message, "remediation": v.remediation, "compliance_refs": v.compliance_refs} for v in violations]
|
|
178
|
+
click.echo(json.dumps(output, indent=2))
|
|
179
|
+
|
|
180
|
+
threshold_map = {"critical": Severity.CRITICAL, "high": Severity.HIGH, "any": Severity.LOW}
|
|
181
|
+
threshold = threshold_map[fail_on]
|
|
182
|
+
blocking = [v for v in violations if v.severity.value >= threshold.value]
|
|
183
|
+
if blocking:
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ── iris register ──────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
@cli.command()
|
|
190
|
+
@click.option("--name", prompt="Agent name", help="Agent identifier (kebab-case)")
|
|
191
|
+
@click.option("--owner", prompt="Owner email", help="Owner email address")
|
|
192
|
+
@click.option("--team", prompt="Team name", help="Team or squad name")
|
|
193
|
+
@click.option("--env", multiple=True, default=["dev"], help="Environments (repeatable)")
|
|
194
|
+
@click.option("--high-risk", is_flag=True, default=False, help="Flag as high-risk AI (Colorado AI Act)")
|
|
195
|
+
@click.option("--compliance", "-c", multiple=True, help="Compliance frameworks to tag")
|
|
196
|
+
@click.option("--dir", "output_dir", type=Path, default=None)
|
|
197
|
+
def register(name, owner, team, env, high_risk, compliance, output_dir):
|
|
198
|
+
"""
|
|
199
|
+
Register a new agent — creates passport.yaml in the governance directory.
|
|
200
|
+
|
|
201
|
+
Creates the full GitOps governance structure for a new agent:
|
|
202
|
+
governance/agents/<name>/passport.yaml
|
|
203
|
+
governance/agents/<name>/policy-intent.md (template)
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
iris register --name payment-agent --owner alice@co.com --team platform
|
|
207
|
+
"""
|
|
208
|
+
from iris import IrisAgent, DataClassification, ComplianceTag
|
|
209
|
+
|
|
210
|
+
compliance_list = list(compliance) or ["colorado-ai-act"]
|
|
211
|
+
output = output_dir or (Path.cwd() / "governance" / "agents" / name)
|
|
212
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
agent = IrisAgent(
|
|
215
|
+
name=name,
|
|
216
|
+
owner=owner,
|
|
217
|
+
team=team,
|
|
218
|
+
compliance=compliance_list,
|
|
219
|
+
environments=list(env),
|
|
220
|
+
is_high_risk_ai=high_risk,
|
|
221
|
+
policy_dir=output,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Write passport
|
|
225
|
+
(output / "passport.yaml").write_text(agent.passport.to_yaml())
|
|
226
|
+
|
|
227
|
+
# Write intent template
|
|
228
|
+
intent_template = f"""# Policy Intent — {name}
|
|
229
|
+
|
|
230
|
+
> Edit this file to describe what your agent is allowed to do.
|
|
231
|
+
> Then run: iris policy compile --agent {name}
|
|
232
|
+
|
|
233
|
+
## What this agent does
|
|
234
|
+
[Describe the agent's purpose here]
|
|
235
|
+
|
|
236
|
+
## What it is allowed to access
|
|
237
|
+
[List the tools, APIs, and data sources this agent needs]
|
|
238
|
+
|
|
239
|
+
## What it must never do
|
|
240
|
+
[List explicit prohibitions]
|
|
241
|
+
|
|
242
|
+
## Compliance notes
|
|
243
|
+
[Any compliance-specific context]
|
|
244
|
+
"""
|
|
245
|
+
(output / "policy-intent.md").write_text(intent_template)
|
|
246
|
+
|
|
247
|
+
console.print(Panel(
|
|
248
|
+
f"[bold green]✓ Agent registered[/bold green]\n\n"
|
|
249
|
+
f"Name: [cyan]{name}[/cyan]\n"
|
|
250
|
+
f"Passport: {output / 'passport.yaml'}\n"
|
|
251
|
+
f"Intent template: {output / 'policy-intent.md'}\n\n"
|
|
252
|
+
f"Next step: Edit [bold]{output / 'policy-intent.md'}[/bold]\n"
|
|
253
|
+
f"Then run: [bold]iris policy compile --agent {name}[/bold]",
|
|
254
|
+
style="green"
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── iris policy ────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
@cli.group()
|
|
261
|
+
def policy():
|
|
262
|
+
"""Policy management commands."""
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@policy.command("compile")
|
|
267
|
+
@click.option("--agent", required=True, help="Agent name")
|
|
268
|
+
@click.option("--intent", type=Path, default=None, help="Path to policy-intent.md")
|
|
269
|
+
@click.option("--dir", "governance_dir", type=Path, default=None)
|
|
270
|
+
@click.option("--dry-run", is_flag=True, help="Show generated Cedar without writing to disk")
|
|
271
|
+
def policy_compile(agent, intent, governance_dir, dry_run):
|
|
272
|
+
"""
|
|
273
|
+
Compile a natural language policy-intent.md to Cedar.
|
|
274
|
+
|
|
275
|
+
Reads the policy-intent.md for the agent, sends it to the IRIS
|
|
276
|
+
policy compiler (your LLM — see ~/.iris/config.yaml), validates
|
|
277
|
+
against compliance bundles, and writes the generated Cedar.
|
|
278
|
+
|
|
279
|
+
Always caches policy-draft.cedar for offline `iris policy diff`.
|
|
280
|
+
|
|
281
|
+
Requires your API key: ANTHROPIC_API_KEY or OPENAI_API_KEY.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
iris policy compile --agent payment-agent
|
|
285
|
+
iris policy compile --agent payment-agent --dry-run
|
|
286
|
+
"""
|
|
287
|
+
from pathlib import Path
|
|
288
|
+
|
|
289
|
+
from iris_core.models.passport import AgentPassport
|
|
290
|
+
|
|
291
|
+
from iris_cli.compiler_config import compiler_info, create_policy_compiler
|
|
292
|
+
from iris_cli.policy_cache import save_policy_draft
|
|
293
|
+
|
|
294
|
+
gov_dir = governance_dir or Path.cwd() / "governance" / "agents" / agent
|
|
295
|
+
passport_file = gov_dir / "passport.yaml"
|
|
296
|
+
intent_file = intent or gov_dir / "policy-intent.md"
|
|
297
|
+
|
|
298
|
+
if not passport_file.exists():
|
|
299
|
+
console.print(f"[red]Passport not found: {passport_file}[/red]")
|
|
300
|
+
console.print(f"Run: iris register --name {agent}")
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
|
|
303
|
+
if not intent_file.exists():
|
|
304
|
+
console.print(f"[red]Intent file not found: {intent_file}[/red]")
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
passport = AgentPassport.from_yaml(passport_file.read_text())
|
|
308
|
+
intent_text = intent_file.read_text()
|
|
309
|
+
|
|
310
|
+
with console.status(f"[bold blue]Compiling policy for {agent}...[/bold blue]"):
|
|
311
|
+
compiler = create_policy_compiler()
|
|
312
|
+
result = compiler.compile(intent_text, passport)
|
|
313
|
+
|
|
314
|
+
if result.has_blocking_violations():
|
|
315
|
+
console.print("\n[bold red]Policy compilation blocked[/bold red]")
|
|
316
|
+
for v in result.violations:
|
|
317
|
+
console.print(f"\n[red]✗ {v.rule_id}[/red]: {v.message}")
|
|
318
|
+
console.print(f" [yellow]Remediation:[/yellow] {v.remediation}")
|
|
319
|
+
console.print("\n[yellow]Contact your security engineer to resolve these violations.[/yellow]")
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
|
|
322
|
+
backend, model = compiler_info(compiler)
|
|
323
|
+
draft_path = save_policy_draft(
|
|
324
|
+
gov_dir, intent_text, result.cedar_policy, backend, model
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if dry_run:
|
|
328
|
+
console.print("\n[bold]Generated Cedar policy (dry run):[/bold]")
|
|
329
|
+
console.print(result.cedar_policy)
|
|
330
|
+
console.print(f"\n[dim]Draft cached: {draft_path}[/dim]")
|
|
331
|
+
console.print(f"[dim]Run: iris policy diff --agent {agent}[/dim]")
|
|
332
|
+
else:
|
|
333
|
+
(gov_dir / "policy.cedar").write_text(result.cedar_policy)
|
|
334
|
+
console.print(f"[bold green]✓ Policy compiled: {gov_dir / 'policy.cedar'}[/bold green]")
|
|
335
|
+
console.print(f"[dim]Draft cached: {draft_path}[/dim]")
|
|
336
|
+
|
|
337
|
+
if result.warnings:
|
|
338
|
+
for w in result.warnings:
|
|
339
|
+
console.print(f"[yellow]Warning:[/yellow] {w}")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
from iris_cli.policy_diff import policy_diff
|
|
343
|
+
policy.add_command(policy_diff)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ── iris license ───────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
@cli.group()
|
|
349
|
+
def license():
|
|
350
|
+
"""IRIS Pro license management."""
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@license.command("activate")
|
|
355
|
+
@click.argument("key")
|
|
356
|
+
def license_activate(key: str):
|
|
357
|
+
"""
|
|
358
|
+
Activate an IRIS Pro license key.
|
|
359
|
+
|
|
360
|
+
Validates the key format and saves it to ~/.iris/license.key.
|
|
361
|
+
You can also set IRIS_LICENSE_KEY in your environment.
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
iris license activate IRIS-XXXX-XXXX-XXXX-XXXX
|
|
365
|
+
"""
|
|
366
|
+
from iris_core.compliance.license import (
|
|
367
|
+
IrisLicense,
|
|
368
|
+
validate_license_key_format,
|
|
369
|
+
write_license_key,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
key = key.strip()
|
|
373
|
+
if not validate_license_key_format(key):
|
|
374
|
+
console.print(
|
|
375
|
+
"[red]Invalid license key format.[/red]\n"
|
|
376
|
+
"Expected: IRIS-XXXX-XXXX-XXXX-XXXX (uppercase letters and digits)\n"
|
|
377
|
+
"Get a license: https://iris.ai/pricing"
|
|
378
|
+
)
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
path = write_license_key(key)
|
|
382
|
+
result = IrisLicense().check("gdpr")
|
|
383
|
+
expiry = result.expires_at or "—"
|
|
384
|
+
console.print(
|
|
385
|
+
Panel(
|
|
386
|
+
f"[bold green]✓ License activated[/bold green]\n\n"
|
|
387
|
+
f"Tier: [cyan]{result.tier}[/cyan]\n"
|
|
388
|
+
f"Expires: {expiry}\n"
|
|
389
|
+
f"Saved to: {path}\n\n"
|
|
390
|
+
f"Pro bundles unlocked: gdpr, hipaa, soc2",
|
|
391
|
+
style="green",
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@license.command("status")
|
|
397
|
+
def license_status():
|
|
398
|
+
"""
|
|
399
|
+
Show current license status and available compliance bundles.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
iris license status
|
|
403
|
+
"""
|
|
404
|
+
from iris_core.compliance.license import (
|
|
405
|
+
FREE_BUNDLES,
|
|
406
|
+
PAID_BUNDLES,
|
|
407
|
+
IrisLicense,
|
|
408
|
+
_read_license_key,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
key = _read_license_key()
|
|
412
|
+
result = IrisLicense().check("gdpr")
|
|
413
|
+
if result.valid:
|
|
414
|
+
status_line = f"[bold green]Active[/bold green] — tier [cyan]{result.tier}[/cyan]"
|
|
415
|
+
if result.expires_at:
|
|
416
|
+
status_line += f", expires {result.expires_at}"
|
|
417
|
+
key_display = f"{key[:9]}…{key[-4:]}" if key and len(key) > 16 else (key or "—")
|
|
418
|
+
elif result.reason == "no_key":
|
|
419
|
+
status_line = "[yellow]No license key[/yellow] — free bundles only"
|
|
420
|
+
key_display = "—"
|
|
421
|
+
else:
|
|
422
|
+
status_line = f"[red]Invalid license[/red] ({result.reason})"
|
|
423
|
+
key_display = "—"
|
|
424
|
+
|
|
425
|
+
free_list = ", ".join(sorted(FREE_BUNDLES))
|
|
426
|
+
paid_list = ", ".join(sorted(PAID_BUNDLES))
|
|
427
|
+
paid_status = (
|
|
428
|
+
"[green]available[/green]"
|
|
429
|
+
if result.valid
|
|
430
|
+
else "[dim]requires Pro license[/dim]"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
console.print(
|
|
434
|
+
Panel(
|
|
435
|
+
f"{status_line}\n"
|
|
436
|
+
f"Key: {key_display}\n\n"
|
|
437
|
+
f"[bold]Free bundles:[/bold] {free_list}\n"
|
|
438
|
+
f"[bold]Pro bundles:[/bold] {paid_list} ({paid_status})",
|
|
439
|
+
title="IRIS License",
|
|
440
|
+
style="blue",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ── iris compliance ────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
@cli.group()
|
|
448
|
+
def compliance():
|
|
449
|
+
"""Compliance checking and reporting commands."""
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
# Wire in the assess command
|
|
453
|
+
from iris_cli.assess import compliance_assess
|
|
454
|
+
compliance.add_command(compliance_assess)
|
|
455
|
+
|
|
456
|
+
# Wire in evidence commands
|
|
457
|
+
from iris_cli.evidence import evidence
|
|
458
|
+
cli.add_command(evidence)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@compliance.command("check")
|
|
462
|
+
@click.option("--agent", default=None, help="Specific agent to check (or all)")
|
|
463
|
+
@click.option("--framework", "-f", default="colorado-ai-act")
|
|
464
|
+
@click.option("--dir", "governance_dir", type=Path, default=None)
|
|
465
|
+
def compliance_check(agent, framework, governance_dir):
|
|
466
|
+
"""
|
|
467
|
+
Check an agent against a compliance framework.
|
|
468
|
+
|
|
469
|
+
Shows a detailed breakdown of which rules pass and fail,
|
|
470
|
+
with plain-English remediation guidance for each failure.
|
|
471
|
+
|
|
472
|
+
Example:
|
|
473
|
+
iris compliance check --framework colorado-ai-act
|
|
474
|
+
iris compliance check --agent payment-agent --framework hipaa
|
|
475
|
+
"""
|
|
476
|
+
from iris_core.compliance.registry import ComplianceRegistry
|
|
477
|
+
from iris import AgentPassport
|
|
478
|
+
|
|
479
|
+
gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
|
|
480
|
+
registry = ComplianceRegistry()
|
|
481
|
+
|
|
482
|
+
passports_to_check = []
|
|
483
|
+
if agent:
|
|
484
|
+
passport_file = gov_dir / agent / "passport.yaml"
|
|
485
|
+
if passport_file.exists():
|
|
486
|
+
passports_to_check.append(AgentPassport.from_yaml(passport_file.read_text()))
|
|
487
|
+
else:
|
|
488
|
+
for f in gov_dir.rglob("passport.yaml"):
|
|
489
|
+
try:
|
|
490
|
+
passports_to_check.append(AgentPassport.from_yaml(f.read_text()))
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
if not passports_to_check:
|
|
495
|
+
console.print("[yellow]No agent passports found.[/yellow]")
|
|
496
|
+
sys.exit(0)
|
|
497
|
+
|
|
498
|
+
all_pass = True
|
|
499
|
+
for passport in passports_to_check:
|
|
500
|
+
violations = registry.check_passport(passport, framework)
|
|
501
|
+
status = "[bold green]PASS[/bold green]" if not violations else "[bold red]FAIL[/bold red]"
|
|
502
|
+
console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
|
|
503
|
+
|
|
504
|
+
if violations:
|
|
505
|
+
all_pass = False
|
|
506
|
+
for v in violations:
|
|
507
|
+
console.print(f" [red]✗[/red] [{v.rule_id}] {v.message}")
|
|
508
|
+
console.print(f" [yellow]→[/yellow] {v.remediation}")
|
|
509
|
+
else:
|
|
510
|
+
console.print(f" [green]✓[/green] All {framework} rules satisfied")
|
|
511
|
+
|
|
512
|
+
for note in registry.last_check_notes:
|
|
513
|
+
console.print(f" [dim]ℹ {note}[/dim]")
|
|
514
|
+
|
|
515
|
+
sys.exit(0 if all_pass else 1)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
cli()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@cli.group()
|
|
523
|
+
def mcp():
|
|
524
|
+
"""MCP server commands for Cursor IDE integration."""
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@mcp.command("start")
|
|
529
|
+
def mcp_start():
|
|
530
|
+
"""
|
|
531
|
+
Start the IRIS MCP server for Cursor IDE.
|
|
532
|
+
|
|
533
|
+
Cursor connects to this server to get real-time compliance
|
|
534
|
+
feedback as you write agent code.
|
|
535
|
+
|
|
536
|
+
Example:
|
|
537
|
+
iris mcp start
|
|
538
|
+
"""
|
|
539
|
+
from iris_cli.mcp_server import start
|
|
540
|
+
console.print("[bold blue]IRIS MCP server starting on stdio...[/bold blue]")
|
|
541
|
+
console.print("[dim]Cursor is now connected to IRIS governance.[/dim]")
|
|
542
|
+
start()
|