Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ Then point any OpenAI-compatible client at `http://localhost:8080/v1`.

```bash
pip install air-blackbox # Core SDK + tracing + compliance
air-blackbox setup # Pulls the AI model (~8GB, one-time)
```

That's it. Two commands and you're scanning.

The `setup` command pulls our fine-tuned compliance model ([airblackbox/air-compliance](https://ollama.com/airblackbox/air-compliance)) from the Ollama registry. It requires [Ollama](https://ollama.com) to be installed first.

If you skip setup, the scanner still works — it just uses regex-only checks instead of the AI model. First time you run `air-blackbox comply`, it will auto-pull the model for you.

**Framework extras:**

```bash
pip install "air-blackbox[langchain]" # + LangChain / LangGraph trust layer
pip install "air-blackbox[openai]" # + OpenAI client wrapper
pip install "air-blackbox[crewai]" # + CrewAI trust layer
Expand All @@ -125,10 +137,12 @@ Non-blocking callback handlers that observe and log — they never control or bl
| Framework | Install | Status |
|---|---|---|
| LangChain / LangGraph | `pip install "air-blackbox[langchain]"` | ✅ Full |
| OpenAI SDK | `pip install "air-blackbox[openai]"` | ✅ Full |
| CrewAI | `pip install "air-blackbox[crewai]"` | 🔧 Scaffold |
| AutoGen | `pip install "air-blackbox[autogen]"` | 🔧 Scaffold |
| Google ADK | `pip install "air-blackbox[adk]"` | 🔧 Scaffold |
| OpenAI Agents SDK | `pip install "air-blackbox[openai]"` | ✅ Full |
| CrewAI | `pip install "air-blackbox[crewai]"` | ✅ Full |
| AutoGen | `pip install "air-blackbox[autogen]"` | ✅ Full |
| Google ADK | `pip install "air-blackbox[adk]"` | ✅ Full |
| Haystack | `pip install "air-blackbox[haystack]"` | ✅ Full |
| Claude Agent SDK | `pip install "air-blackbox[claude]"` | ✅ Full |

Every trust layer includes:

Expand Down
341 changes: 341 additions & 0 deletions docs/vaultmind.html

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rewrites": [
{ "source": "/vaultmind", "destination": "/vaultmind.html" },
{ "source": "/vaultmind/", "destination": "/vaultmind.html" }
]
}
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ dependencies = [
[project.optional-dependencies]
langchain = ["langchain-core>=0.2.0"]
crewai = ["crewai>=0.50.0"]
haystack = ["haystack-ai>=2.0.0"]
openai = ["openai>=1.0"]
autogen = ["autogen-agentchat>=0.4.0"]
adk = ["google-adk>=0.1.0"]
claude = ["claude-agent-sdk>=0.1.0"]
all = [
"air-blackbox[langchain]",
"air-blackbox[crewai]",
"air-blackbox[haystack]",
"air-blackbox[openai]",
"air-blackbox[autogen]",
"air-blackbox[claude]",
Expand Down
15 changes: 15 additions & 0 deletions sdk/air_blackbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,18 @@ def attach(self, agent):
elif framework == "crewai":
from air_blackbox.trust.crewai import attach_trust
return attach_trust(agent, self.gateway_url)
elif framework == "haystack":
from air_blackbox.trust.haystack import attach_trust
return attach_trust(agent, self.gateway_url)
elif framework == "openai":
from air_blackbox.trust.openai_agents import attach_trust
return attach_trust(agent, self.gateway_url)
elif framework == "autogen":
from air_blackbox.trust.autogen import attach_trust
return attach_trust(agent, self.gateway_url)
elif framework == "adk":
from air_blackbox.trust.adk import attach_trust
return attach_trust(agent, self.gateway_url)
elif framework == "claude_agent":
from air_blackbox.trust.claude_agent import attach_trust
return attach_trust(agent, self.gateway_url)
Expand All @@ -106,6 +112,8 @@ def _detect_framework(self, agent):
return "langchain"
elif "crewai" in agent_type:
return "crewai"
elif "haystack" in agent_type:
return "haystack"
elif "openai" in agent_type:
return "openai"
elif "autogen" in agent_type:
Expand All @@ -115,4 +123,11 @@ def _detect_framework(self, agent):
elif "claude_agent_sdk" in agent_type or "claude_agent" in agent_type:
return "claude_agent"

# Fallback: check class name for common patterns
cls_name = type(agent).__name__
if cls_name == "Pipeline" and hasattr(agent, "run"):
return "haystack"
elif cls_name == "Crew" and hasattr(agent, "kickoff"):
return "crewai"

return "unknown"
205 changes: 188 additions & 17 deletions sdk/air_blackbox/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
AIR Blackbox CLI — AI governance control plane.

air-blackbox setup # One-command setup: install model + verify
air-blackbox discover # Shadow AI inventory + AI-BOM
air-blackbox comply # EU AI Act compliance from live traffic
air-blackbox replay # Incident reconstruction from audit chain
Expand Down Expand Up @@ -31,6 +32,87 @@ def main():
pass


@main.command()
def setup():
"""One-command setup: install the AI compliance model and verify everything works.

This pulls the air-compliance model from Ollama registry and verifies
the scanner is ready to use. Run this once after installing air-blackbox.

Requirements: Ollama must be installed first (https://ollama.com)
"""
import subprocess
import shutil

console.print(Panel.fit(
"[bold cyan]AIR Blackbox Setup[/bold cyan]\n"
"Setting up the AI compliance scanner...",
border_style="cyan",
))

# Step 1: Check Ollama
console.print("\n[bold]Step 1/3:[/bold] Checking Ollama installation...")
if shutil.which("ollama"):
try:
result = subprocess.run(["ollama", "--version"], capture_output=True, text=True, timeout=5)
console.print(f" [green]✓[/green] Ollama installed: {result.stdout.strip()}")
except Exception:
console.print(" [green]✓[/green] Ollama found")
else:
console.print(" [red]✗[/red] Ollama not installed")
console.print("\n Install Ollama first:")
console.print(" Mac: [cyan]brew install ollama[/cyan]")
console.print(" Linux: [cyan]curl -fsSL https://ollama.com/install.sh | sh[/cyan]")
console.print(" All: [cyan]https://ollama.com/download[/cyan]")
console.print("\n Then run [cyan]air-blackbox setup[/cyan] again.")
return

# Step 2: Pull model
console.print("\n[bold]Step 2/3:[/bold] Pulling air-compliance model from registry...")
console.print(" This downloads ~8GB (one-time). Grab a coffee.\n")

try:
result = subprocess.run(
["ollama", "pull", "airblackbox/air-compliance"],
timeout=600,
)
if result.returncode == 0:
# Create local alias
subprocess.run(
["ollama", "cp", "airblackbox/air-compliance", "air-compliance"],
capture_output=True, timeout=30,
)
console.print(" [green]✓[/green] Model pulled and ready")
else:
console.print(" [red]✗[/red] Failed to pull model")
console.print(" Try manually: [cyan]ollama pull airblackbox/air-compliance[/cyan]")
return
except subprocess.TimeoutExpired:
console.print(" [red]✗[/red] Download timed out. Try: [cyan]ollama pull airblackbox/air-compliance[/cyan]")
return

# Step 3: Verify
console.print("\n[bold]Step 3/3:[/bold] Verifying scanner...")
try:
result = subprocess.run(["ollama", "list"], capture_output=True, text=True, timeout=10)
if "air-compliance" in result.stdout:
console.print(" [green]✓[/green] Model verified in Ollama")
else:
console.print(" [yellow]⚠[/yellow] Model pulled but not showing in list. Try restarting Ollama.")
except Exception:
# Best-effort verification; do not fail setup if this check errors
console.print(" [yellow]⚠[/yellow] Could not verify model in Ollama (verification step failed).")

console.print(Panel.fit(
"[bold green]Setup complete![/bold green]\n\n"
"Run your first scan:\n"
" [cyan]air-blackbox comply --scan .[/cyan]\n\n"
"Or try the demo:\n"
" [cyan]air-blackbox demo[/cyan]",
border_style="green",
))


@main.command()
@click.option("--gateway", default="http://localhost:8080", help="Gateway URL")
@click.option("--scan", default=".", help="Path to scan for code-level checks")
Expand Down Expand Up @@ -68,16 +150,19 @@ def comply(gateway, scan, runs_dir, fmt, verbose, deep, no_llm, model, no_save):
import os
if _ollama_available() and _model_available(model):
console.print(f"[bold]Running hybrid analysis (regex + AI model)...[/]\n")
# Collect all Python files
# Collect all Python files (supports single-file and directory scanning)
py_files = []
skip_dirs = {"node_modules", ".git", "__pycache__", ".venv", "venv",
"dist", "build", ".eggs", "site-packages", ".tox",
".mypy_cache", ".pytest_cache"}
for root, dirs, files in os.walk(scan):
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.endswith(".egg-info")]
for f in files:
if f.endswith(".py"):
py_files.append(os.path.join(root, f))
if os.path.isfile(scan) and scan.endswith(".py"):
py_files = [os.path.abspath(scan)]
else:
skip_dirs = {"node_modules", ".git", "__pycache__", ".venv", "venv",
"dist", "build", ".eggs", "site-packages", ".tox",
".mypy_cache", ".pytest_cache"}
for root, dirs, files in os.walk(scan):
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.endswith(".egg-info")]
for f in files:
if f.endswith(".py"):
py_files.append(os.path.join(root, f))
total_files = len(py_files)

# === Smart sampling: pick compliance-relevant files ===
Expand Down Expand Up @@ -172,17 +257,101 @@ def _score_file(fp):
if files_included > 5:
console.print(f" [dim]... and {files_included - 5} more files[/]")

if verbose:
os.environ["AIR_VERBOSE"] = "1"
result = deep_scan(merged_code, model=model,
sample_context=sample_desc,
total_files=total_files)
if verbose:
os.environ.pop("AIR_VERBOSE", None)
# Build rule-based context summary for the model
rule_context_lines = []
article_map = {9: "Risk Management", 10: "Data Governance",
11: "Technical Documentation", 12: "Record-Keeping",
14: "Human Oversight", 15: "Accuracy & Security"}
for article in articles:
art_num = article.get("number", 0)
if art_num not in article_map:
continue
passes = []
fails = []
warns = []
for check in article.get("checks", []):
name = check.get("name", "")
evidence = check.get("evidence", "")
status = check.get("status", "")
summary = f"{name}: {evidence[:80]}" if evidence else name
if status == "pass":
passes.append(summary)
elif status == "fail":
fails.append(summary)
elif status == "warn":
warns.append(summary)
line = f"Article {art_num} ({article_map[art_num]}): "
if passes:
line += f"{len(passes)} PASS ({'; '.join(passes[:2])})"
if fails:
line += f", {len(fails)} FAIL" if passes else f"{len(fails)} FAIL"
if warns:
line += f", {len(warns)} WARN" if (passes or fails) else f"{len(warns)} WARN"
rule_context_lines.append(line)
rule_context = "\n".join(rule_context_lines)

# Only run AI model if we have actual code to analyze
if files_included == 0 or not merged_code.strip():
if verbose:
console.print(f" [dim]No Python files found for AI analysis — skipping model[/]")
result = {"available": False, "findings": [], "model": model, "error": None}
else:
if verbose:
os.environ["AIR_VERBOSE"] = "1"
result = deep_scan(merged_code, model=model,
sample_context=sample_desc,
total_files=total_files,
rule_context=rule_context)
if verbose:
os.environ.pop("AIR_VERBOSE", None)
if result.get("available") and not result.get("error"):
deep_findings = result.get("findings", [])

# ── Smart reconciliation: override model FAIL when rule-based has strong PASS ──
# Build a map of rule-based pass counts per article
rule_pass_counts = {}
rule_evidence_map = {}
for article in articles:
art_num = article.get("number", 0)
passes = [c for c in article.get("checks", []) if c.get("status") == "pass"]
rule_pass_counts[art_num] = len(passes)
if passes:
# Collect the best evidence summaries
rule_evidence_map[art_num] = "; ".join(
c.get("evidence", "")[:60] for c in passes[:3]
)

overrides = 0
for finding in deep_findings:
art = finding.get("article", 0)
model_status = finding.get("status", "")
rule_passes = rule_pass_counts.get(art, 0)

# If model says FAIL but rule-based has 2+ PASS checks → override to PASS
if model_status == "fail" and rule_passes >= 2:
finding["status"] = "pass"
rule_ev = rule_evidence_map.get(art, "")
finding["evidence"] = (
f"[Corrected by rule-based analysis] "
f"Rule-based scanner found {rule_passes} passing checks: {rule_ev}. "
f"Model's original assessment: {finding.get('evidence', '')}"
)
finding["fix_hint"] = ""
overrides += 1
# If model says FAIL but rule-based has 1 PASS → upgrade to WARN
elif model_status == "fail" and rule_passes == 1:
finding["status"] = "warn"
rule_ev = rule_evidence_map.get(art, "")
finding["evidence"] = (
f"[Partial — rule-based found evidence] {rule_ev}. "
f"Model noted: {finding.get('evidence', '')}"
)
overrides += 1

console.print(f" [green]●[/] AI model analyzed [bold]{files_included}[/] files ({total_chars:,} chars) from {total_files} total")
console.print(f" [green]●[/] AI model found [bold]{len(deep_findings)}[/] finding(s) using [bold]{model}[/]")
if overrides > 0:
console.print(f" [green]●[/] Smart reconciliation: [bold]{overrides}[/] model verdict(s) corrected by rule-based evidence")
console.print(f" [green]●[/] Hybrid mode: rule-based + AI analysis merged\n")
elif result.get("error"):
console.print(f" [yellow]●[/] AI model: {result['error']}")
Expand All @@ -201,7 +370,9 @@ def _score_file(fp):
if verbose:
console.print(f" [dim]Saved to compliance history (scan #{scan_id})[/]\n")
except Exception:
pass # Don't break the scan if history fails
# Don't break the scan if history save fails
if verbose:
console.print(" [dim]Could not save to compliance history[/]")

if fmt == "json":
import json
Expand Down
Loading
Loading