From dffecbeb641caef4fc592dccf402ae087ea03524 Mon Sep 17 00:00:00 2001 From: EDAO-TECH Date: Tue, 30 Sep 2025 10:49:52 +0930 Subject: [PATCH 1/2] Add go-to-market collateral docs --- docs/doc_index.md | 8 ++- docs/financial_projections_2025_2027.md | 53 ++++++++++++++++++++ docs/marketing_site_copy.md | 65 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 docs/financial_projections_2025_2027.md create mode 100644 docs/marketing_site_copy.md diff --git a/docs/doc_index.md b/docs/doc_index.md index a7b6502..4e868da 100644 --- a/docs/doc_index.md +++ b/docs/doc_index.md @@ -15,6 +15,12 @@ `QA_Healing_Script_Checklist.md` ## Strategy & Vision -- **Official Whitepaper** +- **Official Whitepaper** `SELFIX_Whitepaper.md` +## Go-To-Market Assets +- **Website Copy Playbook** + `marketing_site_copy.md` +- **Three-Year Financial Projections** + `financial_projections_2025_2027.md` + diff --git a/docs/financial_projections_2025_2027.md b/docs/financial_projections_2025_2027.md new file mode 100644 index 0000000..7a497b4 --- /dev/null +++ b/docs/financial_projections_2025_2027.md @@ -0,0 +1,53 @@ +# Selfix ProHealers 3-Year Financial Projections + +## Scope & Currency +- Focus: Enterprise business for Australian healthcare-led entry motion. +- Pricing anchored to USD with regional display toggle for AUD. +- Includes subscription revenue, add-ons, and services in first year ramp. + +## Key Assumptions (Base Case) +- **New Customer Logos:** 6 in Year 1, 16 in Year 2, 20 in Year 3 via 14-day pilots and partner-assisted sales. +- **Plan Mix:** 40% Core, 45% Resilience, 15% Regulated across the customer base. +- **Churn:** 6% annually. +- **Expansion:** 12% net revenue retention uplift per year from workload/artifact growth and retention upgrades. +- **Add-on Attach Rates per Logo:** On-chain anchoring 25%; DFIR Playbooks 20%; 24×7 support 30%; average add-on revenue ≈ $13k per logo annually. +- **Services Uptake (Year 1 per new logo):** QuickStart (60%), Compliance Mapping (40%), On-site/Air-gapped install (15%). +- **Subscription COGS:** 18% of revenue, yielding ~82% gross margin. +- **Operating Expense Mix:** Sales & Marketing 35%→28%; R&D 22%→18%; G&A 12%→10% from Year 1 to Year 3. +- **Pricing:** Core $48k, Resilience $120k, Regulated $220k (USD) with listed overage and retention policies. + +## Projected P&L Summary (USD) +### Year 1 +- **Subscription ARR:** $676k (2 Core, 3 Resilience, 1 Regulated customers). +- **Add-ons:** ~$78k. +- **Services:** ~$86k. +- **Total Revenue:** ~$840k. +- **COGS (18%):** ~$151k. +- **Gross Margin:** ~$689k (~82%). +- **Operating Expenses:** S&M ~$294k; R&D ~$185k; G&A ~$101k. +- **EBIT:** ~$109k. + +### Year 2 +- **Customer Base:** 22 logos after 16 net new additions and nominal churn. +- **Revenue:** ~$4.71M total (includes 12% expansion on prior subscriptions). +- **COGS (18%):** ~$0.85M. +- **Gross Margin:** ~$3.86M (~82%). +- **Operating Expenses:** S&M ~$1.51M; R&D ~$0.99M; G&A ~$0.47M. +- **EBIT:** ~$0.89M. + +### Year 3 +- **Customer Base:** ~36 logos after 20 net new additions and churn impact. +- **Revenue:** ~$9.57M total with continued 12% expansion uplift. +- **COGS (18%):** ~$1.72M. +- **Gross Margin:** ~$7.85M (~82%). +- **Operating Expenses:** S&M ~$2.68M; R&D ~$1.72M; G&A ~$0.96M. +- **EBIT:** ~$2.49M. + +## Sensitivity Analysis +- **Low Case:** Slower new logos (4/10/14), add-on average $9k, expansion uplift 6% → Year 3 revenue ≈ $6.2M, EBIT ≈ $0.9M. +- **High Case:** Faster new logos (8/22/28), add-on average $17k, expansion uplift 15% → Year 3 revenue ≈ $12.4M, EBIT ≈ $3.6M. + +## Execution Notes +- Highlight outcome-backed SLAs on pricing assets to reinforce value. +- Promote estimator-driven ROI, especially SIEM offload savings, in sales materials. +- Ensure compliance messaging ties to OAIC APPs, HIPAA/HITECH, SOX/GLBA/PCI crosswalks to support regulated buyers. diff --git a/docs/marketing_site_copy.md b/docs/marketing_site_copy.md new file mode 100644 index 0000000..c02093d --- /dev/null +++ b/docs/marketing_site_copy.md @@ -0,0 +1,65 @@ +# Selfix ProHealers Website Copy + +## Home Page +- **Hero** + - **Headline:** "Seal what’s sacred. Prove what healed." + - **Subheadline:** "Trusted Logic + Immutable Forgiveness for regulated environments." + - **CTAs:** "See 10-min demo" and "Start a 14-day pilot" +- **Outcome Bar:** "Audit pack ≤15 min • Auto-heal MTTR ≤5 min • SIEM offload 20–30% • Air-gapped edition" +- **Value Pillars:** + 1. Sealed Memory (trusted logic) + 2. Forensic Ledger (approvals + signatures) + 3. Privacy by Design (tombstones) +- **How It Works:** Seal → Validate → Forgive & Prove (CI/CD, ITSM, SIEM, KMS/HSM) +- **ICP Tabs:** Public Sector • Healthcare • FinServ (link to Solutions pages) +- **Integrations Marquee:** Jira • ServiceNow • Splunk • Elastic • CrowdStrike/SentinelOne • KMS/HSM • SBOM • CI/CD +- **Resources & CTA Strip:** Download Sample Signed Report • Watch 10-min demo • Start a 14-day pilot + +## Product Page (ProHealers) +- **Hero:** "Seal what’s sacred. Prove what healed. Adjacent proof layer—keep EDR/Backups/SIEM, add proof." +- **Problem → Why Now:** "Backups restore bytes; auditors need proof. SIEM bloat; slow evidence; sensitive fixes." +- **Solution Pillars:** + 1. Sealed Memory — auto-restore only trusted logic + 2. Forensic Ledger — approvals + signed exports + retention + 3. Privacy — tombstones +- **How It Works:** Seal → Validate → Forgive & Prove with CI/CD hooks, ITSM approvals, SIEM summaries, KMS/HSM integration, optional air-gapped deployment +- **Outcome Targets / SLA Badges:** Audit pack ≤15 min • MTTR ≤5 min • SIEM offload 20–30% • Air-gapped ready +- **Integrations & Compliance Tabs:** ITSM • CI/CD • SIEM • EDR • KMS/HSM plus compliance sections for Public Sector (NIST/FOIA), Healthcare (HIPAA/HITECH), and FinServ (SOX/GLBA/PCI) with "Download sample signed export" CTA +- **Final CTA:** "Start a 14-day pilot" and "Talk to an expert" + +## Solutions — Healthcare Page +- **Hero:** "Clean restores you can defend to auditors—without exposing PHI." +- **Why It Matters:** + - Audit pack in minutes + - PHI-safe tombstone redactions + - Air-gapped option for high-assurance sites +- **What We Deliver:** + - Sealed Memory for EHR/clinical scripts/configs/IaC + - Forensic Ledger with approvals + context; signed PDF/JSON; 1/3/7-year retention +- **How It Works:** CI/CD hooks • ITSM approvals • SIEM summaries • KMS/HSM; Seal → Validate → Forgive & Prove +- **Outcome Targets:** ≤15-min audit pack • ≤5-min auto-heal MTTR • 20–30% SIEM offload +- **Call to Action:** "Start a Healthcare POV (14-days)" + +## Pricing Page +- **Hero:** "Simple plans. Strong assurances. Start with Core; scale by workloads, artifacts, retention, integrations." +- **Annual Plans (USD):** + - **Core — $48,000/yr:** 50 workloads • 200 sealed artifacts • 1-year retention • 1 auditor seat • signed exports • core integrations + - **Resilience — $120,000/yr:** 200 workloads • 1,000 artifacts • 3-year retention • 3 seats • SSO/SCIM • advanced integrations • outcome SLAs + - **Regulated / Air-Gapped — $220,000/yr:** 300 workloads • unlimited artifacts* • 7-year retention • offline keys • 24×7 + TAM • compliance pack (*fair-use, architecture review) +- **Outcome-backed SLAs:** Audit pack: Core ≤30 min; Resilience/Regulated ≤15 min • Auto-heal MTTR: Core ≤10 min; Resilience/Regulated ≤5 min • Sev-1 response: Core next business day; Resilience 4h; Regulated 24×7/1h +- **Estimator / Calculator Inputs:** workloads • artifacts • retention (1/3/7 years) • auditor seats • on-chain anchoring (Y/N) • SIEM savings (GB/yr, $/GB, offload%) +- **Estimator Outputs:** suggested plan • overages/add-ons • annual total • total minus savings +- **Add-ons & Services:** On-chain anchoring $12k/yr or $0.20/event (min $500/mo); Extra auditor seat $3k/yr; DFIR Playbooks $15k/yr; 24×7 support $18k–$30k/yr; QuickStart $12k; Compliance Mapping $8k; On-site/Air-gapped install $25k +- **Overages & Policies:** $600/workload/yr (packs of 10); $60/artifact/yr (packs of 50); +$12,000/yr per +2 years of retention (up to 7 years); Multi-year discounts (2-yr −10%, 3-yr −15%); Savings-share at renewal (≤15% ACV); Regional: local currency display; data-residency +$12k/yr; FX clause +- **FAQ:** + - Do you store PHI/PII? No—logic + metadata only. + - Is on-chain mandatory? No—optional hash anchoring. + - Air-gapped? Yes—offline keys + local ledger. +- **Information Architecture Reminder:** Maintain top-level navigation: Home • Product • Solutions • Pricing • Resources • Partners • Security & Compliance • Docs • Contact/Start a Pilot + +## Execution Checklist +- Place Outcome-backed SLAs table directly under plan cards with credit language. +- Embed pricing estimator with SIEM-savings inputs and "Request Private Offer" option. +- Keep adjacent proof layer positioning consistent across Product & Pricing pages. +- Maintain the specified top-level information architecture. + From 8adb8a2a7f34f868138b391a6609fd8bb8726e4a Mon Sep 17 00:00:00 2001 From: EDAO-TECH Date: Tue, 30 Sep 2025 11:17:46 +0930 Subject: [PATCH 2/2] Add automation tooling and container stack --- ai_engine/agents_registry.json | 4 ++ docker-compose.yml | 72 +++++++++++++++++++++++++ docker/backend.Dockerfile | 29 ++++++++++ docker/frontend.Dockerfile | 38 +++++++++++++ docker/nginx.conf | 83 ++++++++++++++++++++++++++++ scripts/cleanup_selfix.sh | 51 ++++++++++++++++++ scripts/generate_registry.py | 99 ++++++++++++++++++++++++++++++++++ tools/basic_tests.py | 62 +++++++++++++++++++++ 8 files changed, 438 insertions(+) create mode 100644 ai_engine/agents_registry.json create mode 100644 docker-compose.yml create mode 100644 docker/backend.Dockerfile create mode 100644 docker/frontend.Dockerfile create mode 100644 docker/nginx.conf create mode 100755 scripts/cleanup_selfix.sh create mode 100755 scripts/generate_registry.py create mode 100755 tools/basic_tests.py diff --git a/ai_engine/agents_registry.json b/ai_engine/agents_registry.json new file mode 100644 index 0000000..84c3a06 --- /dev/null +++ b/ai_engine/agents_registry.json @@ -0,0 +1,4 @@ +{ + "agents": [], + "total_agents": 0 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..05d950c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +version: "3.9" + +services: + backend: + build: + context: . + dockerfile: docker/backend.Dockerfile + image: selfix/backend:latest + container_name: selfix-backend + restart: unless-stopped + env_file: + - .env + environment: + UVICORN_WORKERS: 2 + expose: + - "8000" + networks: + - selfix + volumes: + - backend-logs:/app/logs + + frontend: + build: + context: . + dockerfile: docker/frontend.Dockerfile + image: selfix/frontend:latest + container_name: selfix-frontend + restart: unless-stopped + expose: + - "80" + networks: + - selfix + + nginx: + image: nginx:1.25-alpine + container_name: selfix-gateway + restart: unless-stopped + depends_on: + - backend + - frontend + ports: + - "80:80" + - "443:443" + volumes: + - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro + - certbot-webroot:/var/www/certbot + - certbot-certs:/etc/letsencrypt + networks: + - selfix + + # Optional shared services for caching or persistence. + redis: + image: redis:7-alpine + container_name: selfix-redis + restart: unless-stopped + networks: + - selfix + volumes: + - redis-data:/data + command: ["redis-server", "--save", "", "--appendonly", "no"] + profiles: + - infra + +volumes: + backend-logs: + certbot-webroot: + certbot-certs: + redis-data: + +networks: + selfix: + driver: bridge diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 0000000..be4055b --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Install system dependencies required for building Python packages. +RUN apt-get update \ + && apt-get install --no-install-recommends -y build-essential curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy dependency files separately to leverage Docker layer caching. +COPY requirements.txt ./ +RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi + +# Copy the remainder of the application source. +COPY . . + +# Create a non-root user for running the service. +RUN useradd -m selfix && chown -R selfix:selfix /app +USER selfix + +EXPOSE 8000 + +# Use gunicorn/uvicorn to serve the FastAPI application. +CMD ["uvicorn", "main_api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile new file mode 100644 index 0000000..0dc0b03 --- /dev/null +++ b/docker/frontend.Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-alpine AS build +WORKDIR /frontend + +# Install dependencies. +COPY frontend/package*.json ./ +RUN npm ci + +# Copy application source and build the production bundle. +COPY frontend . +RUN npm run build + +FROM nginx:1.25-alpine AS runtime + +# Copy the static build output into the Nginx html directory. +COPY --from=build /frontend/dist /usr/share/nginx/html + +# Provide a default Nginx configuration suitable for SPAs. +COPY <<'NGINX_CONF' /etc/nginx/conf.d/default.conf +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + access_log /var/log/nginx/frontend_access.log; + error_log /var/log/nginx/frontend_error.log warn; +} +NGINX_CONF + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..3f00b6d --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,83 @@ +events {} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/proxy_access.log main; + error_log /var/log/nginx/proxy_error.log warn; + + sendfile on; + keepalive_timeout 65; + + upstream backend_service { + server backend:8000; + } + + upstream frontend_service { + server frontend:80; + } + + server { + listen 80; + server_name _; + + # Redirect ACME HTTP-01 validation requests to the local certbot volume. + location /.well-known/acme-challenge/ { + alias /var/www/certbot/.well-known/acme-challenge/; + } + + location /api/ { + proxy_pass http://backend_service$request_uri; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://frontend_service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + # HTTPS server block (certificates provisioned by Certbot). + server { + listen 443 ssl http2; + server_name _; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + location /api/ { + proxy_pass http://backend_service$request_uri; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://frontend_service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /.well-known/acme-challenge/ { + alias /var/www/certbot/.well-known/acme-challenge/; + } + } +} diff --git a/scripts/cleanup_selfix.sh b/scripts/cleanup_selfix.sh new file mode 100755 index 0000000..028feb5 --- /dev/null +++ b/scripts/cleanup_selfix.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# cleanup_selfix.sh - remove Python build artefacts and temporary agent folders. +# +# Usage: +# ./scripts/cleanup_selfix.sh # perform cleanup +# ./scripts/cleanup_selfix.sh --dry-run # preview actions without deleting files +# +set -euo pipefail + +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true +fi + +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$ROOT_DIR" + +mapfile -t PY_CACHE_DIRS < <(find . -type d -name '__pycache__') +mapfile -t PYC_FILES < <(find . -type f -name '*.py[co]') +mapfile -t TEMP_AGENT_DIRS < <(find . -type d -name 'agents_tmp') + +remove_item() { + local path="$1" + if [[ $DRY_RUN == true ]]; then + printf '[dry-run] would remove %s\n' "$path" + else + if [[ -d $path ]]; then + rm -rf "$path" + printf 'removed directory %s\n' "$path" + elif [[ -f $path ]]; then + rm -f "$path" + printf 'removed file %s\n' "$path" + fi + fi +} + +for path in "${PY_CACHE_DIRS[@]}"; do + remove_item "$path" +done + +for path in "${PYC_FILES[@]}"; do + remove_item "$path" +done + +for path in "${TEMP_AGENT_DIRS[@]}"; do + remove_item "$path" +done + +printf '\nCleanup complete. %d cache directories, %d compiled files, %d temporary agent folders processed.\n' \ + "${#PY_CACHE_DIRS[@]}" "${#PYC_FILES[@]}" "${#TEMP_AGENT_DIRS[@]}" diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py new file mode 100755 index 0000000..2ae5ec1 --- /dev/null +++ b/scripts/generate_registry.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Generate an agents registry by discovering Agent classes dynamically.""" + +from __future__ import annotations + +import argparse +import ast +import json +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Dict, Iterable, List + +DEFAULT_AGENT_ROOT = Path("ai_engine") / "agents" +DEFAULT_OUTPUT = Path("ai_engine") / "agents_registry.json" + + +@dataclass +class AgentDefinition: + name: str + module: str + file: str + + @classmethod + def from_module(cls, class_name: str, module_path: Path, agent_root: Path) -> "AgentDefinition": + relative_module = module_path.with_suffix("").relative_to(agent_root) + module = ".".join(relative_module.parts) + return cls(name=class_name, module=module, file=str(module_path)) + + +def discover_agent_classes(agent_root: Path) -> List[AgentDefinition]: + if not agent_root.exists(): + return [] + + discovered: Dict[str, AgentDefinition] = {} + + for python_file in agent_root.rglob("*.py"): + if python_file.name == "__init__.py": + continue + + try: + tree = ast.parse(python_file.read_text(), filename=str(python_file)) + except SyntaxError as exc: + raise SyntaxError(f"Unable to parse {python_file}: {exc}") from exc + + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name.endswith("Agent") and not node.name.startswith("_"): + definition = AgentDefinition.from_module(node.name, python_file, agent_root) + key = f"{definition.module}:{definition.name}" + discovered[key] = definition + + return sorted(discovered.values(), key=lambda item: (item.module, item.name)) + + +def write_registry(definitions: Iterable[AgentDefinition], output_path: Path) -> None: + definition_list = list(definitions) + registry = { + "agents": [asdict(agent) for agent in definition_list], + "total_agents": len(definition_list), + } + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(registry, indent=2)) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate the Selfix agent registry") + parser.add_argument( + "--root", + type=Path, + default=DEFAULT_AGENT_ROOT, + help="Root directory containing agent definitions (default: ai_engine/agents)", + ) + parser.add_argument( + "--output", + type=Path, + default=DEFAULT_OUTPUT, + help="Path to write the registry JSON file (default: ai_engine/agents_registry.json)", + ) + parser.add_argument( + "--print", + dest="should_print", + action="store_true", + help="Print the generated registry to stdout in addition to writing the file", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + agents = discover_agent_classes(args.root) + write_registry(agents, args.output) + + if args.should_print: + print(json.dumps({"agents": [asdict(agent) for agent in agents]}, indent=2)) + + print(f"Registry written to {args.output} ({len(agents)} agents discovered)") + + +if __name__ == "__main__": + main() diff --git a/tools/basic_tests.py b/tools/basic_tests.py new file mode 100755 index 0000000..fa42d2c --- /dev/null +++ b/tools/basic_tests.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Lightweight sanity tests for Selfix utilities.""" + +from __future__ import annotations + +import json +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from scripts.generate_registry import AgentDefinition, discover_agent_classes, write_registry # noqa: E402 + + +class GenerateRegistryTestCase(unittest.TestCase): + def test_discover_returns_empty_when_root_missing(self) -> None: + missing = ROOT / "not_a_real_directory" + self.assertEqual(discover_agent_classes(missing), []) + + def test_discover_finds_agent_classes(self) -> None: + with TemporaryDirectory() as tmp: + agent_root = Path(tmp) + module_path = agent_root / "business" + module_path.mkdir(parents=True) + agent_file = module_path / "example_agent.py" + agent_file.write_text( + """ +class ExampleAgent: + pass + +class AnotherHelper: + pass + +class ComplianceAgent: + pass +""" + ) + + agents = discover_agent_classes(agent_root) + names = {agent.name for agent in agents} + self.assertIn("ExampleAgent", names) + self.assertIn("ComplianceAgent", names) + self.assertNotIn("AnotherHelper", names) + + def test_write_registry_outputs_expected_json(self) -> None: + with TemporaryDirectory() as tmp: + output = Path(tmp) / "registry.json" + agents = [ + AgentDefinition(name="TestAgent", module="demo.test_agent", file="demo/test_agent.py") + ] + write_registry(agents, output) + data = json.loads(output.read_text()) + self.assertEqual(data["total_agents"], 1) + self.assertEqual(data["agents"][0]["name"], "TestAgent") + + +if __name__ == "__main__": + unittest.main()