aigis-cli 1.0.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.
Files changed (53) hide show
  1. aigis_cli/SKILL.md +74 -0
  2. aigis_cli/__init__.py +3 -0
  3. aigis_cli/annotate.py +45 -0
  4. aigis_cli/classify.py +192 -0
  5. aigis_cli/cli.py +280 -0
  6. aigis_cli/content/implement/audit-logging.md +171 -0
  7. aigis_cli/content/implement/bias-monitoring.md +172 -0
  8. aigis_cli/content/implement/confidence-scoring.md +180 -0
  9. aigis_cli/content/implement/data-integrity.md +257 -0
  10. aigis_cli/content/implement/explainability.md +174 -0
  11. aigis_cli/content/implement/fallback-patterns.md +190 -0
  12. aigis_cli/content/implement/human-oversight.md +220 -0
  13. aigis_cli/content/implement/input-validation.md +212 -0
  14. aigis_cli/content/implement/monitoring.md +207 -0
  15. aigis_cli/content/implement/output-sanitization.md +183 -0
  16. aigis_cli/content/implement/pii-handling.md +207 -0
  17. aigis_cli/content/implement/prompt-security.md +153 -0
  18. aigis_cli/content/implement/rag-security.md +230 -0
  19. aigis_cli/content/implement/rate-limiting.md +217 -0
  20. aigis_cli/content/implement/supply-chain.md +193 -0
  21. aigis_cli/content/index/audit-scan.md +403 -0
  22. aigis_cli/content/index/frameworks.md +82 -0
  23. aigis_cli/content/index/guardrails.json +23 -0
  24. aigis_cli/content/index/taxonomy.md +81 -0
  25. aigis_cli/content/templates/ai-impact-assessment.md +54 -0
  26. aigis_cli/content/templates/intended-purpose-doc.md +39 -0
  27. aigis_cli/content/templates/risk-characterization.md +34 -0
  28. aigis_cli/content/templates/third-party-assessment.md +41 -0
  29. aigis_cli/content/verify/checklist-audit-logging.md +13 -0
  30. aigis_cli/content/verify/checklist-bias-monitoring.md +13 -0
  31. aigis_cli/content/verify/checklist-confidence-scoring.md +13 -0
  32. aigis_cli/content/verify/checklist-data-integrity.md +12 -0
  33. aigis_cli/content/verify/checklist-explainability.md +13 -0
  34. aigis_cli/content/verify/checklist-fallback-patterns.md +13 -0
  35. aigis_cli/content/verify/checklist-human-oversight.md +14 -0
  36. aigis_cli/content/verify/checklist-input-validation.md +16 -0
  37. aigis_cli/content/verify/checklist-monitoring.md +13 -0
  38. aigis_cli/content/verify/checklist-output-sanitization.md +14 -0
  39. aigis_cli/content/verify/checklist-pii-handling.md +14 -0
  40. aigis_cli/content/verify/checklist-prompt-security.md +13 -0
  41. aigis_cli/content/verify/checklist-rag-security.md +12 -0
  42. aigis_cli/content/verify/checklist-rate-limiting.md +13 -0
  43. aigis_cli/content/verify/checklist-supply-chain.md +13 -0
  44. aigis_cli/fetch.py +99 -0
  45. aigis_cli/init_ide.py +107 -0
  46. aigis_cli/keywords.py +136 -0
  47. aigis_cli/search.py +69 -0
  48. aigis_cli-1.0.0.dist-info/METADATA +59 -0
  49. aigis_cli-1.0.0.dist-info/RECORD +53 -0
  50. aigis_cli-1.0.0.dist-info/WHEEL +5 -0
  51. aigis_cli-1.0.0.dist-info/entry_points.txt +2 -0
  52. aigis_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  53. aigis_cli-1.0.0.dist-info/top_level.txt +1 -0
aigis_cli/SKILL.md ADDED
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: aigis
3
+ description: AI governance skill for coding agents. Provides framework-aligned security and compliance patterns from NIST AI RMF, OWASP Top 10 for LLMs, and ISO/IEC 42001. Use before building any AI/LLM feature or when auditing existing AI systems.
4
+ ---
5
+
6
+ # Aigis — AI Governance Skill
7
+
8
+ Use the aigis CLI for governance patterns when building or auditing AI/LLM features.
9
+
10
+ ## Quick start — new feature
11
+ \`\`\`bash
12
+ # 1. Classify your system to get relevant controls
13
+ aigis classify --traits uses-llm,accepts-user-input,processes-pii,is-external --json
14
+
15
+ # 2. Fetch implementation patterns for each recommended file
16
+ aigis get input-validation pii-handling prompt-security audit-logging
17
+
18
+ # 3. Write code applying the patterns from the fetched files
19
+
20
+ # 4. After writing code, run verification
21
+ aigis verify input-validation pii-handling prompt-security audit-logging
22
+
23
+ # 5. If risk tier is HIGH or MEDIUM, generate compliance docs
24
+ aigis template ai-impact-assessment intended-purpose-doc
25
+ \`\`\`
26
+
27
+ ## Quick start — audit existing project
28
+ \`\`\`bash
29
+ # 1. Get the structured audit scan prompt
30
+ aigis audit --scan
31
+
32
+ # 2. Follow the scan instructions: inventory the project, detect traits, classify
33
+
34
+ # 3. Run the full audit with detected traits
35
+ aigis audit --traits uses-llm,processes-pii,is-external
36
+
37
+ # 4. Evaluate existing code against each check, produce gap report
38
+ \`\`\`
39
+
40
+ ## When to use
41
+ - Before writing any LLM API call or AI feature (new code)
42
+ - When auditing an existing AI system for governance gaps (existing code)
43
+ - Before processing any sensitive data through an AI system
44
+ - Before deploying any AI feature to production
45
+ - When onboarding to a new AI project to understand what controls exist
46
+
47
+ ## Commands
48
+ - \`aigis classify --traits <comma-separated>\` — get risk tier and relevant files
49
+ - \`aigis classify "<description>"\` — same, using natural language (keyword matching)
50
+ - \`aigis get <file-id> [file-id...]\` — fetch implementation patterns (one or more)
51
+ - \`aigis get <file-id> --lang py|js\` — fetch filtered to one language
52
+ - \`aigis verify <file-id> [file-id...]\` — fetch verification checklists
53
+ - \`aigis template <template-id> [template-id...]\` — fetch compliance documentation templates
54
+ - \`aigis audit --scan\` — get structured audit prompt for scanning existing codebases
55
+ - \`aigis audit --traits <comma-separated>\` — get bundled classification + all checklists for audit
56
+ - \`aigis search <query>\` — search across all content by keyword or control ID
57
+ - \`aigis search --list\` — list all available files
58
+ - \`aigis annotate <file-id> "<note>"\` — attach a local note for future sessions
59
+ - \`aigis annotate --list\` — list all annotations
60
+ - \`aigis init cursor|claude-code|windsurf|copilot\` — set up aigis for your IDE
61
+
62
+ ## Available traits (22)
63
+ AI architecture: uses-llm, uses-rag, uses-finetuned, uses-thirdparty-api, is-agentic, is-multimodal
64
+ Data sensitivity: processes-pii, handles-financial, handles-health, handles-proprietary, handles-minors
65
+ Impact scope: influences-decisions, accepts-user-input, is-external, is-internal, is-high-volume
66
+ Output type: generates-code, generates-content, multi-model-pipeline
67
+ Jurisdiction: jurisdiction-eu, jurisdiction-us-regulated, jurisdiction-global
68
+
69
+ ## Integration
70
+ - Claude Code: place this file in ~/.claude/skills/aigis/SKILL.md
71
+ - Cursor: run \`aigis init cursor\` in your project root
72
+ - Windsurf: run \`aigis init windsurf\` in your project root
73
+ - GitHub Copilot: run \`aigis init copilot\` in your project root
74
+ - Any agent: include aigis usage instructions in system prompt
aigis_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """aigis-cli: AI governance guardrails for coding agents."""
2
+
3
+ __version__ = "1.0.0"
aigis_cli/annotate.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import date
5
+ from pathlib import Path
6
+
7
+ AIGIS_DIR = Path.home() / ".aigis"
8
+ ANNOTATIONS_FILE = AIGIS_DIR / "annotations.json"
9
+
10
+
11
+ def _load_annotations() -> dict:
12
+ if not ANNOTATIONS_FILE.exists():
13
+ return {}
14
+ try:
15
+ return json.loads(ANNOTATIONS_FILE.read_text(encoding="utf-8"))
16
+ except (json.JSONDecodeError, OSError):
17
+ return {}
18
+
19
+
20
+ def _save_annotations(data: dict) -> None:
21
+ AIGIS_DIR.mkdir(parents=True, exist_ok=True)
22
+ ANNOTATIONS_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
23
+
24
+
25
+ def annotate(file_id: str, note: str) -> None:
26
+ data = _load_annotations()
27
+ data.setdefault(file_id, []).append({
28
+ "note": note,
29
+ "date": date.today().isoformat(),
30
+ })
31
+ _save_annotations(data)
32
+
33
+
34
+ def get_annotations(file_id: str) -> list[dict]:
35
+ return _load_annotations().get(file_id, [])
36
+
37
+
38
+ def list_annotations() -> dict:
39
+ return _load_annotations()
40
+
41
+
42
+ def clear_annotation(file_id: str) -> None:
43
+ data = _load_annotations()
44
+ data.pop(file_id, None)
45
+ _save_annotations(data)
aigis_cli/classify.py ADDED
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ ALL_TRAITS = [
4
+ "uses-llm", "uses-rag", "uses-finetuned", "uses-thirdparty-api", "is-agentic", "is-multimodal",
5
+ "processes-pii", "handles-financial", "handles-health", "handles-proprietary", "handles-minors",
6
+ "influences-decisions", "accepts-user-input", "is-external", "is-internal", "is-high-volume",
7
+ "generates-code", "generates-content", "multi-model-pipeline",
8
+ "jurisdiction-eu", "jurisdiction-us-regulated", "jurisdiction-global",
9
+ ]
10
+
11
+ TRAIT_FILES: dict[str, list[str]] = {
12
+ "uses-llm": ["input-validation", "output-sanitization", "prompt-security", "audit-logging", "monitoring"],
13
+ "uses-rag": ["rag-security", "data-integrity"],
14
+ "uses-finetuned": ["data-integrity", "supply-chain"],
15
+ "uses-thirdparty-api": ["supply-chain"],
16
+ "is-agentic": ["human-oversight", "rate-limiting", "fallback-patterns", "audit-logging"],
17
+ "is-multimodal": ["input-validation", "output-sanitization", "pii-handling"],
18
+ "processes-pii": ["pii-handling", "audit-logging"],
19
+ "handles-financial": ["pii-handling", "audit-logging", "bias-monitoring"],
20
+ "handles-health": ["pii-handling", "audit-logging", "bias-monitoring", "explainability"],
21
+ "handles-proprietary": ["pii-handling", "prompt-security"],
22
+ "handles-minors": ["pii-handling", "bias-monitoring", "human-oversight"],
23
+ "influences-decisions": ["bias-monitoring", "confidence-scoring", "human-oversight", "explainability"],
24
+ "accepts-user-input": ["input-validation", "output-sanitization"],
25
+ "is-external": ["rate-limiting", "confidence-scoring"],
26
+ "is-internal": [],
27
+ "is-high-volume": ["rate-limiting", "monitoring", "fallback-patterns"],
28
+ "generates-code": ["output-sanitization", "human-oversight", "fallback-patterns"],
29
+ "generates-content": ["confidence-scoring", "bias-monitoring", "audit-logging"],
30
+ "multi-model-pipeline": ["audit-logging", "monitoring", "fallback-patterns", "data-integrity"],
31
+ "jurisdiction-eu": [],
32
+ "jurisdiction-us-regulated": [],
33
+ "jurisdiction-global": [],
34
+ }
35
+
36
+ FILE_CONTROLS: dict[str, dict[str, list[str]]] = {
37
+ "input-validation": {"owasp": ["LLM01"], "nist": ["MEASURE-2.7", "MANAGE-1.3"], "iso": ["Clause-8.2", "Annex-A.6"]},
38
+ "output-sanitization": {"owasp": ["LLM05"], "nist": ["MEASURE-2.6", "MEASURE-2.7"], "iso": ["Clause-8.2"]},
39
+ "pii-handling": {"owasp": ["LLM02"], "nist": ["MAP-2.1", "MEASURE-2.10"], "iso": ["Annex-A.7", "Annex-A.4"]},
40
+ "prompt-security": {"owasp": ["LLM07"], "nist": ["MEASURE-2.7", "MEASURE-2.8"], "iso": ["Clause-8.2"]},
41
+ "human-oversight": {"owasp": ["LLM06"], "nist": ["MAP-3.5", "MANAGE-1.3", "MANAGE-4.1"], "iso": ["Annex-A.9", "Clause-8.4"]},
42
+ "supply-chain": {"owasp": ["LLM03"], "nist": ["GOVERN-6.1", "GOVERN-6.2", "MANAGE-3.1", "MANAGE-3.2"], "iso": ["Annex-A.10"]},
43
+ "data-integrity": {"owasp": ["LLM04"], "nist": ["MAP-2.3", "MEASURE-2.6"], "iso": ["Annex-A.7"]},
44
+ "rag-security": {"owasp": ["LLM08"], "nist": ["MEASURE-2.7"], "iso": ["Clause-8.2", "Annex-A.7"]},
45
+ "confidence-scoring": {"owasp": ["LLM09"], "nist": ["MAP-2.2", "MEASURE-2.5", "MEASURE-2.9"], "iso": ["Annex-A.8"]},
46
+ "rate-limiting": {"owasp": ["LLM10"], "nist": ["MEASURE-2.6", "MANAGE-2.4"], "iso": ["Clause-8.2"]},
47
+ "audit-logging": {"owasp": [], "nist": ["MEASURE-2.8", "MANAGE-4.1", "MANAGE-4.3"], "iso": ["Clause-9.1", "Annex-A.6"]},
48
+ "bias-monitoring": {"owasp": [], "nist": ["MAP-2.3", "MEASURE-2.11", "MEASURE-3.1"], "iso": ["Clause-6.1", "Annex-C"]},
49
+ "fallback-patterns": {"owasp": [], "nist": ["MEASURE-2.6", "MANAGE-2.3", "MANAGE-2.4"], "iso": ["Clause-8.2"]},
50
+ "monitoring": {"owasp": [], "nist": ["MEASURE-2.4", "MEASURE-3.1", "MANAGE-4.1", "MANAGE-4.2"], "iso": ["Clause-9.1", "Clause-10"]},
51
+ "explainability": {"owasp": [], "nist": ["MEASURE-2.8", "MEASURE-2.9"], "iso": ["Annex-A.8", "Clause-7.4"]},
52
+ }
53
+
54
+
55
+ def classify(trait_list: list[str]) -> dict:
56
+ ts = set(trait_list)
57
+ warnings: list[str] = []
58
+
59
+ invalid = [t for t in trait_list if t not in ALL_TRAITS]
60
+ if invalid:
61
+ raise ValueError(
62
+ f'Unknown traits: {", ".join(invalid)}. Run "aigis traits" to see available traits.'
63
+ )
64
+
65
+ # Constraint C1: is-internal + is-external
66
+ if "is-internal" in ts and "is-external" in ts:
67
+ warnings.append("Both is-internal and is-external selected. Treating as is-external (stricter).")
68
+ ts.discard("is-internal")
69
+
70
+ # Step 1: trait-based file selection
71
+ files: set[str] = set()
72
+ for t in ts:
73
+ for f in TRAIT_FILES.get(t, []):
74
+ files.add(f)
75
+
76
+ # Step 2: risk tier
77
+ sens_data = bool(
78
+ ts & {"processes-pii", "handles-financial", "handles-health", "handles-proprietary", "handles-minors"}
79
+ )
80
+
81
+ tier = "LOW"
82
+ reason = "no high/medium triggers"
83
+
84
+ if "influences-decisions" in ts:
85
+ tier, reason = "HIGH", "influences-decisions"
86
+ elif "handles-health" in ts:
87
+ tier, reason = "HIGH", "handles-health"
88
+ elif "handles-financial" in ts and "accepts-user-input" in ts:
89
+ tier, reason = "HIGH", "handles-financial + accepts-user-input"
90
+ elif "handles-minors" in ts:
91
+ tier, reason = "HIGH", "handles-minors"
92
+ elif ("jurisdiction-eu" in ts or "jurisdiction-global" in ts) and sens_data:
93
+ tier, reason = "HIGH", "jurisdiction-eu + sensitive data"
94
+ elif "generates-code" in ts and "is-external" in ts:
95
+ tier, reason = "HIGH", "generates-code + is-external"
96
+ elif "generates-code" in ts and "is-agentic" in ts:
97
+ tier, reason = "HIGH", "generates-code + is-agentic"
98
+ elif "processes-pii" in ts:
99
+ tier, reason = "MEDIUM", "processes-pii"
100
+ elif "is-external" in ts:
101
+ tier, reason = "MEDIUM", "is-external"
102
+ elif "is-agentic" in ts:
103
+ tier, reason = "MEDIUM", "is-agentic"
104
+ elif "handles-proprietary" in ts:
105
+ tier, reason = "MEDIUM", "handles-proprietary"
106
+ elif "generates-content" in ts:
107
+ tier, reason = "MEDIUM", "generates-content"
108
+ elif "multi-model-pipeline" in ts:
109
+ tier, reason = "MEDIUM", "multi-model-pipeline"
110
+ elif "jurisdiction-us-regulated" in ts:
111
+ tier, reason = "MEDIUM", "jurisdiction-us-regulated"
112
+ elif "generates-code" in ts:
113
+ tier, reason = "MEDIUM", "generates-code"
114
+
115
+ # Jurisdiction modifier
116
+ if ("jurisdiction-eu" in ts or "jurisdiction-global" in ts) and tier != "HIGH":
117
+ old_tier = tier
118
+ tier = "MEDIUM" if tier == "LOW" else "HIGH"
119
+ reason += f" (elevated from {old_tier} by EU/global jurisdiction)"
120
+
121
+ # Step 3: guardrails
122
+ guardrails_fired: list[dict] = []
123
+
124
+ # G12 removal guardrail fires first
125
+ if "uses-llm" in ts and "uses-thirdparty-api" not in ts and "supply-chain" in files:
126
+ files.discard("supply-chain")
127
+ guardrails_fired.append({
128
+ "id": "G12",
129
+ "action": "REMOVE supply-chain",
130
+ "rationale": "Self-hosted models skip third-party controls",
131
+ })
132
+
133
+ guardrails = [
134
+ ("G1", lambda: sens_data and "audit-logging" not in files, "audit-logging", "Sensitive data requires traceability"),
135
+ ("G2", lambda: "handles-health" in ts and "bias-monitoring" not in files, "bias-monitoring", "Health data has demographic bias risks"),
136
+ ("G3", lambda: "handles-financial" in ts and "influences-decisions" in ts and "fallback-patterns" not in files, "fallback-patterns", "Financial decisions need safe failure"),
137
+ ("G4", lambda: "is-agentic" in ts and "human-oversight" not in files, "human-oversight", "Autonomous systems need oversight"),
138
+ ("G5", lambda: "generates-code" in ts and "output-sanitization" not in files, "output-sanitization", "Generated code is execution risk"),
139
+ ("G6", lambda: "jurisdiction-eu" in ts and "explainability" not in files, "explainability", "EU AI Act requires explainability"),
140
+ ("G7", lambda: "jurisdiction-eu" in ts and "bias-monitoring" not in files, "bias-monitoring", "EU AI Act mandates non-discrimination"),
141
+ ("G8", lambda: "handles-minors" in ts and "human-oversight" not in files, "human-oversight", "Systems affecting children require review"),
142
+ ("G9", lambda: "multi-model-pipeline" in ts and "monitoring" not in files, "monitoring", "Multi-model compounding failures"),
143
+ ("G10", lambda: tier == "HIGH" and "monitoring" not in files, "monitoring", "High-risk systems need monitoring"),
144
+ ("G11", lambda: tier == "HIGH" and "fallback-patterns" not in files, "fallback-patterns", "High-risk systems must fail safely"),
145
+ ("G13", lambda: "jurisdiction-us-regulated" in ts and "audit-logging" not in files, "audit-logging", "US regulated industries need audit trails"),
146
+ ("G14", lambda: "jurisdiction-us-regulated" in ts and "human-oversight" not in files, "human-oversight", "US regulated industries need human review"),
147
+ ("G15", lambda: "generates-code" in ts and "human-oversight" not in files, "human-oversight", "Code generation needs human review"),
148
+ ]
149
+
150
+ for gid, cond, file, rationale in guardrails:
151
+ if cond():
152
+ files.add(file)
153
+ guardrails_fired.append({"id": gid, "action": f"ADD {file}", "rationale": rationale})
154
+
155
+ # Step 4: templates
156
+ templates: list[str] = []
157
+ if tier == "HIGH" or "jurisdiction-eu" in ts or "jurisdiction-global" in ts or "influences-decisions" in ts:
158
+ templates.extend(["ai-impact-assessment", "intended-purpose-doc", "risk-characterization"])
159
+ elif tier == "MEDIUM" or "jurisdiction-us-regulated" in ts:
160
+ templates.append("intended-purpose-doc")
161
+
162
+ if "uses-thirdparty-api" in ts and tier != "LOW":
163
+ templates.append("third-party-assessment")
164
+
165
+ if "jurisdiction-us-regulated" in ts and "ai-impact-assessment" not in templates:
166
+ templates.insert(0, "ai-impact-assessment")
167
+
168
+ # Step 5: collect control IDs
169
+ all_owasp: set[str] = set()
170
+ all_nist: set[str] = set()
171
+ all_iso: set[str] = set()
172
+ for f in files:
173
+ c = FILE_CONTROLS.get(f)
174
+ if c:
175
+ all_owasp.update(c["owasp"])
176
+ all_nist.update(c["nist"])
177
+ all_iso.update(c["iso"])
178
+
179
+ return {
180
+ "risk_tier": tier,
181
+ "reason": reason,
182
+ "traits": list(ts),
183
+ "implement_files": sorted(files),
184
+ "templates": list(dict.fromkeys(templates)),
185
+ "guardrails_fired": guardrails_fired,
186
+ "warnings": warnings,
187
+ "controls": {
188
+ "owasp": sorted(all_owasp),
189
+ "nist": sorted(all_nist),
190
+ "iso": sorted(all_iso),
191
+ },
192
+ }
aigis_cli/cli.py ADDED
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ from . import __version__
10
+ from .classify import classify as run_classify
11
+ from .fetch import get as fetch_get, get_audit_scan, get_template, verify as fetch_verify
12
+ from .keywords import detect_traits_from_text
13
+ from .search import list_all, search as run_search
14
+ from .annotate import annotate as add_annotation, clear_annotation, list_annotations
15
+ from .init_ide import init as run_init
16
+
17
+ console = Console(highlight=False)
18
+
19
+
20
+ @click.group()
21
+ @click.version_option(__version__, prog_name="aigis")
22
+ def cli() -> None:
23
+ """AI governance guardrails for coding agents."""
24
+
25
+
26
+ # ── classify ────────────────────────────────────────────────────────
27
+ @cli.command()
28
+ @click.argument("description", required=False, default=None)
29
+ @click.option("--traits", "traits_str", default=None, help="Comma-separated trait IDs")
30
+ @click.option("--interactive", is_flag=True, help="Confirm detected traits before classifying")
31
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
32
+ def classify(description: str | None, traits_str: str | None,
33
+ interactive: bool, as_json: bool) -> None:
34
+ """Classify an AI system and get recommended governance files."""
35
+ traits: list[str] | None = None
36
+
37
+ if traits_str:
38
+ traits = [t.strip() for t in traits_str.split(",")]
39
+ elif description:
40
+ detected = detect_traits_from_text(description)
41
+ if interactive:
42
+ console.print("\n[bold]Detected traits from description:[/bold]\n")
43
+ for d in detected:
44
+ icon = "[green]✓[/green]" if d["confidence"] == "high" else "[yellow]?[/yellow]"
45
+ console.print(f" {icon} [cyan]{d['trait']}[/cyan] (matched: \"{d['keyword']}\")")
46
+ trait_list = ",".join(d["trait"] for d in detected)
47
+ console.print("[dim]\nTo classify with these traits, run:[/dim]")
48
+ console.print(f"[green] aigis classify --traits {trait_list}[/green]\n")
49
+ console.print("[dim]Add or remove traits as needed before running.\n[/dim]")
50
+ return
51
+ traits = [d["trait"] for d in detected]
52
+ if not traits:
53
+ console.print("[red]No traits detected from description.[/red]")
54
+ console.print('[dim]Run "aigis traits" to see available traits, or use --traits flag.\n[/dim]')
55
+ sys.exit(1)
56
+ console.print(f"[dim]Detected traits: {', '.join(traits)}\n[/dim]")
57
+ else:
58
+ console.print("[red]Provide --traits or a quoted description.[/red]")
59
+ console.print("[dim]Example: aigis classify --traits uses-llm,processes-pii[/dim]")
60
+ console.print('[dim]Example: aigis classify "customer chatbot with RAG"\n[/dim]')
61
+ sys.exit(1)
62
+
63
+ try:
64
+ result = run_classify(traits)
65
+ except ValueError as exc:
66
+ console.print(f"[red]{exc}[/red]")
67
+ sys.exit(1)
68
+
69
+ if as_json:
70
+ click.echo(json.dumps(result, indent=2))
71
+ return
72
+
73
+ tier = result["risk_tier"]
74
+ tier_color = "red" if tier == "HIGH" else "yellow" if tier == "MEDIUM" else "green"
75
+ console.print(f"[bold]Risk tier:[/bold] [bold {tier_color}]{tier}[/bold {tier_color}]")
76
+ console.print(f"[bold]Reason:[/bold] {result['reason']}\n")
77
+
78
+ for w in result["warnings"]:
79
+ console.print(f"[yellow]⚠ {w}[/yellow]")
80
+ if result["warnings"]:
81
+ console.print()
82
+
83
+ console.print(f"[bold]Implement files ({len(result['implement_files'])}):[/bold]")
84
+ for f in result["implement_files"]:
85
+ console.print(f"[green] aigis get {f}[/green]")
86
+
87
+ if result["templates"]:
88
+ console.print(f"\n[bold]Templates ({len(result['templates'])}):[/bold]")
89
+ for t in result["templates"]:
90
+ console.print(f"[yellow] aigis template {t}[/yellow]")
91
+
92
+ if result["guardrails_fired"]:
93
+ console.print("\n[bold]Guardrails fired:[/bold]")
94
+ for g in result["guardrails_fired"]:
95
+ console.print(f"[yellow] {g['id']}: {g['action']} — {g['rationale']}[/yellow]")
96
+
97
+ console.print("\n[bold]Verify after implementation:[/bold]")
98
+ for f in result["implement_files"]:
99
+ console.print(f"[green] aigis verify {f}[/green]")
100
+
101
+ c = result["controls"]
102
+ console.print(f"\n[dim]Controls: {len(c['owasp'])} OWASP, {len(c['nist'])} NIST, {len(c['iso'])} ISO[/dim]")
103
+
104
+
105
+ # ── get ─────────────────────────────────────────────────────────────
106
+ @cli.command("get")
107
+ @click.argument("files", nargs=-1)
108
+ @click.option("--all", "all_files", is_flag=True, help="Fetch all implement files")
109
+ @click.option("--lang", type=click.Choice(["py", "js"]), default=None, help="Filter code to py or js only")
110
+ @click.option("--no-frontmatter", "no_frontmatter", is_flag=True, help="Strip YAML frontmatter")
111
+ def get_cmd(files: tuple[str, ...], all_files: bool, lang: str | None,
112
+ no_frontmatter: bool) -> None:
113
+ """Fetch implementation pattern files."""
114
+ content = fetch_get(list(files) or None, all_files=all_files,
115
+ strip_fm=not no_frontmatter, lang=lang)
116
+ click.echo(content)
117
+
118
+
119
+ # ── verify ──────────────────────────────────────────────────────────
120
+ @cli.command()
121
+ @click.argument("files", nargs=-1, required=True)
122
+ def verify(files: tuple[str, ...]) -> None:
123
+ """Fetch verification checklists."""
124
+ click.echo(fetch_verify(list(files)))
125
+
126
+
127
+ # ── template ────────────────────────────────────────────────────────
128
+ @cli.command()
129
+ @click.argument("templates", nargs=-1, required=True)
130
+ def template(templates: tuple[str, ...]) -> None:
131
+ """Fetch compliance documentation templates."""
132
+ click.echo(get_template(list(templates)))
133
+
134
+
135
+ # ── audit ───────────────────────────────────────────────────────────
136
+ @cli.command()
137
+ @click.option("--scan", is_flag=True, help="Get structured scan prompt for the agent")
138
+ @click.option("--traits", "traits_str", default=None, help="Run audit with detected traits")
139
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
140
+ def audit(scan: bool, traits_str: str | None, as_json: bool) -> None:
141
+ """Audit an existing codebase for governance gaps."""
142
+ if scan:
143
+ click.echo(get_audit_scan())
144
+ return
145
+
146
+ if traits_str:
147
+ traits = [t.strip() for t in traits_str.split(",")]
148
+ classification = run_classify(traits)
149
+
150
+ if as_json:
151
+ checklists = {}
152
+ for f in classification["implement_files"]:
153
+ checklists[f] = fetch_verify([f])
154
+ click.echo(json.dumps({"classification": classification, "checklists": checklists}, indent=2))
155
+ return
156
+
157
+ tier = classification["risk_tier"]
158
+ tier_color = "red" if tier == "HIGH" else "yellow" if tier == "MEDIUM" else "green"
159
+ console.print("[bold]═══ AIGIS GOVERNANCE AUDIT ═══[/bold]\n")
160
+ console.print(f"[bold]Risk tier:[/bold] [bold {tier_color}]{tier}[/bold {tier_color}]")
161
+ console.print(f"[bold]Controls to assess:[/bold] {len(classification['implement_files'])} areas\n")
162
+
163
+ console.print("[bold]Instructions for agent:[/bold]")
164
+ console.print("[dim]Evaluate the existing codebase against each check below.[/dim]")
165
+ console.print('[dim]Mark PASS / FAIL / PARTIAL with evidence (file:line or "not found").\n[/dim]')
166
+
167
+ for f in classification["implement_files"]:
168
+ checklist = fetch_verify([f])
169
+ click.echo(checklist)
170
+ click.echo()
171
+
172
+ if classification["templates"]:
173
+ console.print("[bold]Required documentation:[/bold]")
174
+ for t in classification["templates"]:
175
+ console.print(f"[yellow] aigis template {t}[/yellow]")
176
+ return
177
+
178
+ console.print("[red]Provide --scan or --traits.[/red]")
179
+ console.print("[dim] aigis audit --scan # get scan prompt[/dim]")
180
+ console.print("[dim] aigis audit --traits uses-llm,... # run audit\n[/dim]")
181
+ sys.exit(1)
182
+
183
+
184
+ # ── search ──────────────────────────────────────────────────────────
185
+ @cli.command()
186
+ @click.argument("query", required=False, default=None)
187
+ @click.option("--list", "list_files", is_flag=True, help="List all available files")
188
+ def search(query: str | None, list_files: bool) -> None:
189
+ """Search across all content."""
190
+ if list_files:
191
+ results = list_all()
192
+ console.print("[bold]Available content:\n[/bold]")
193
+ console.print("[bold]Implement files (governance patterns):[/bold]")
194
+ for r in results["implement"]:
195
+ console.print(f" [cyan]{r['id']:<25}[/cyan] {r['title']}")
196
+ console.print("\n[bold]Templates (compliance documentation):[/bold]")
197
+ for r in results["templates"]:
198
+ console.print(f" [yellow]{r['id']:<25}[/yellow] {r['title']}")
199
+ console.print("\n[bold]Verify checklists:[/bold]")
200
+ console.print("[dim] (one per implement file, auto-fetched via aigis verify <id>)[/dim]")
201
+ return
202
+
203
+ if not query:
204
+ console.print("[red]Provide a search query or use --list.[/red]")
205
+ sys.exit(1)
206
+
207
+ results = run_search(query)
208
+ if not results:
209
+ console.print(f'[dim]No results found for "{query}"[/dim]')
210
+ return
211
+ console.print(f'[bold]Results for "{query}":\n[/bold]')
212
+ for r in results:
213
+ console.print(f" [cyan]{r['id']:<25}[/cyan] {r['title']}")
214
+ if r["matched_controls"]:
215
+ console.print(f" [dim]{'':<25} controls: {', '.join(r['matched_controls'])}[/dim]")
216
+
217
+
218
+ # ── annotate ────────────────────────────────────────────────────────
219
+ @cli.command()
220
+ @click.argument("file_id", required=False, default=None)
221
+ @click.argument("note", required=False, default=None)
222
+ @click.option("--list", "list_all_annotations", is_flag=True, help="List all annotations")
223
+ @click.option("--clear", is_flag=True, help="Clear annotations for a file")
224
+ def annotate(file_id: str | None, note: str | None,
225
+ list_all_annotations: bool, clear: bool) -> None:
226
+ """Attach or manage local notes on content files."""
227
+ if list_all_annotations:
228
+ all_notes = list_annotations()
229
+ if not all_notes:
230
+ console.print("[dim]No annotations yet.[/dim]")
231
+ return
232
+ console.print("[bold]Annotations:\n[/bold]")
233
+ for fid, notes in all_notes.items():
234
+ console.print(f" [cyan]{fid}:[/cyan]")
235
+ for n in notes:
236
+ console.print(f' [dim] "{n["note"]}" ({n["date"]})[/dim]')
237
+ return
238
+
239
+ if not file_id:
240
+ console.print("[red]Provide a file ID. Use --list to see annotations.[/red]")
241
+ sys.exit(1)
242
+
243
+ if clear:
244
+ clear_annotation(file_id)
245
+ console.print(f"[green]Cleared annotations for {file_id}[/green]")
246
+ return
247
+
248
+ if not note:
249
+ console.print("[red]Provide a note in quotes.[/red]")
250
+ console.print('[dim]Example: aigis annotate input-validation "Needs raw body for webhooks"[/dim]')
251
+ sys.exit(1)
252
+
253
+ add_annotation(file_id, note)
254
+ console.print(f"[green]Annotation added to {file_id}[/green]")
255
+
256
+
257
+ # ── init ────────────────────────────────────────────────────────────
258
+ @cli.command("init")
259
+ @click.argument("ide")
260
+ def init_cmd(ide: str) -> None:
261
+ """Set up aigis for your IDE."""
262
+ run_init(ide)
263
+
264
+
265
+ # ── traits ──────────────────────────────────────────────────────────
266
+ @cli.command()
267
+ def traits() -> None:
268
+ """List all available classification traits."""
269
+ console.print("[bold]Available traits (22):\n[/bold]")
270
+ groups = {
271
+ "AI architecture": ["uses-llm", "uses-rag", "uses-finetuned", "uses-thirdparty-api", "is-agentic", "is-multimodal"],
272
+ "Data sensitivity": ["processes-pii", "handles-financial", "handles-health", "handles-proprietary", "handles-minors"],
273
+ "Impact scope": ["influences-decisions", "accepts-user-input", "is-external", "is-internal", "is-high-volume"],
274
+ "Output type": ["generates-code", "generates-content", "multi-model-pipeline"],
275
+ "Jurisdiction": ["jurisdiction-eu", "jurisdiction-us-regulated", "jurisdiction-global"],
276
+ }
277
+ for group, trait_list in groups.items():
278
+ console.print(f" [dim]{group}:[/dim]")
279
+ formatted = ", ".join(f"[cyan]{t}[/cyan]" for t in trait_list)
280
+ console.print(f" {formatted}\n")