Initial commit: Elydora Open Source — self-hosted audit platform for AI agents
Full self-hosted port of the Elydora tamper-evident audit protocol: - Hono API server with PostgreSQL, Redis, MinIO adapters - Ed25519 signed operations, chain hashing, Merkle epoch rollups - PBKDF2 password hashing, HMAC-SHA256 JWT, 5-role RBAC - Next.js management console - Node.js, Python, and Go SDKs - Docker Compose for local deployment - Helm chart for Kubernetes - Comprehensive security hardening and audit fixes applied Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
15547a8c35
commit
e390e68594
207 files changed
+32790
-63
No files matched your search
@@ -0,0 +1,217 @@
|
||||
"""Elydora CLI — install, uninstall, and manage agent audit hooks.
|
||||
|
||||
Entry point: ``elydora`` console script (see pyproject.toml).
|
||||
Uses only stdlib argparse — zero external dependencies for the CLI itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Dict, Type
|
||||
|
||||
from .crypto import get_public_key_base64url
|
||||
from .plugins.base import AgentPlugin, InstallConfig
|
||||
from .plugins.registry import SUPPORTED_AGENTS, get_agent_names
|
||||
from .plugins.hook_template import generate_guard_script
|
||||
from .plugins.claudecode import ClaudeCodePlugin
|
||||
from .plugins.copilot import CopilotPlugin
|
||||
from .plugins.cursor import CursorPlugin
|
||||
from .plugins.gemini import GeminiPlugin
|
||||
from .plugins.kirocli import KiroCliPlugin
|
||||
from .plugins.kiroide import KiroIdePlugin
|
||||
from .plugins.letta import LettaPlugin
|
||||
from .plugins.opencode import OpenCodePlugin
|
||||
|
||||
|
||||
PLUGIN_MAP: Dict[str, Type[AgentPlugin]] = {
|
||||
"claudecode": ClaudeCodePlugin,
|
||||
"copilot": CopilotPlugin,
|
||||
"cursor": CursorPlugin,
|
||||
"gemini": GeminiPlugin,
|
||||
"kirocli": KiroCliPlugin,
|
||||
"kiroide": KiroIdePlugin,
|
||||
"letta": LettaPlugin,
|
||||
"opencode": OpenCodePlugin,
|
||||
}
|
||||
|
||||
|
||||
def _get_plugin(agent_name: str) -> AgentPlugin:
|
||||
"""Instantiate the plugin for the given agent name."""
|
||||
cls = PLUGIN_MAP.get(agent_name)
|
||||
if cls is None:
|
||||
print(f"Error: Unknown agent '{agent_name}'.", file=sys.stderr)
|
||||
print(f"Supported agents: {', '.join(get_agent_names())}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return cls()
|
||||
|
||||
|
||||
def cmd_install(args: argparse.Namespace) -> None:
|
||||
"""Handle the 'install' subcommand."""
|
||||
agent_name: str = args.agent
|
||||
|
||||
# Derive public key to verify the private key is valid
|
||||
try:
|
||||
pub = get_public_key_base64url(args.private_key)
|
||||
except Exception as exc:
|
||||
print(f"Error: Invalid private key — {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Verified key pair (public key: {pub[:16]}...)")
|
||||
|
||||
# Create per-agent directory under ~/.elydora/{agent_id}/
|
||||
agent_dir = os.path.join(os.path.expanduser("~"), ".elydora", args.agent_id)
|
||||
os.makedirs(agent_dir, exist_ok=True)
|
||||
|
||||
# Generate and write the guard script
|
||||
guard_script_path = os.path.join(agent_dir, "guard.py")
|
||||
guard_script = generate_guard_script(agent_name, args.agent_id)
|
||||
with open(guard_script_path, "w", encoding="utf-8") as f:
|
||||
f.write(guard_script)
|
||||
try:
|
||||
import stat
|
||||
os.chmod(guard_script_path, os.stat(guard_script_path).st_mode | stat.S_IEXEC)
|
||||
except Exception:
|
||||
pass # chmod may fail on Windows
|
||||
print(f" Guard script: {guard_script_path}")
|
||||
|
||||
config: InstallConfig = {
|
||||
"org_id": args.org_id,
|
||||
"agent_id": args.agent_id,
|
||||
"agent_name": agent_name,
|
||||
"private_key": args.private_key,
|
||||
"kid": args.kid,
|
||||
"base_url": args.base_url,
|
||||
"guard_script_path": guard_script_path,
|
||||
}
|
||||
if args.token:
|
||||
config["token"] = args.token
|
||||
|
||||
plugin = _get_plugin(agent_name)
|
||||
plugin.install(config)
|
||||
|
||||
|
||||
def cmd_uninstall(args: argparse.Namespace) -> None:
|
||||
"""Handle the 'uninstall' subcommand."""
|
||||
plugin = _get_plugin(args.agent)
|
||||
agent_id = getattr(args, "agent_id", None) or ""
|
||||
plugin.uninstall(agent_id=agent_id)
|
||||
|
||||
# Determine which agent directory to remove
|
||||
elydora_dir = os.path.join(os.path.expanduser("~"), ".elydora")
|
||||
agent_id = getattr(args, "agent_id", None)
|
||||
|
||||
if not agent_id:
|
||||
# Scan ~/.elydora/*/config.json for matching agent_name
|
||||
pattern = os.path.join(elydora_dir, "*", "config.json")
|
||||
for config_path in glob.glob(pattern):
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
if cfg.get("agent_name") == args.agent:
|
||||
agent_id = cfg.get("agent_id")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if agent_id:
|
||||
agent_dir = os.path.join(elydora_dir, agent_id)
|
||||
if os.path.isdir(agent_dir):
|
||||
shutil.rmtree(agent_dir)
|
||||
print(f" Removed agent directory: {agent_dir}")
|
||||
else:
|
||||
# Fallback: try to remove legacy guard script location
|
||||
guard_script_path = os.path.join(
|
||||
elydora_dir, "hooks", f"{args.agent}-guard.py"
|
||||
)
|
||||
if os.path.exists(guard_script_path):
|
||||
os.remove(guard_script_path)
|
||||
print(f" Removed guard script: {guard_script_path}")
|
||||
|
||||
|
||||
def cmd_status(args: argparse.Namespace) -> None:
|
||||
"""Handle the 'status' subcommand."""
|
||||
print("Elydora Agent Hook Status")
|
||||
print("=" * 40)
|
||||
for name in get_agent_names():
|
||||
plugin = PLUGIN_MAP[name]()
|
||||
st = plugin.status()
|
||||
marker = "[installed]" if st["installed"] else "[not installed]"
|
||||
display = SUPPORTED_AGENTS[name]["name"]
|
||||
print(f" {display:20s} {marker:16s} {st['details']}")
|
||||
|
||||
|
||||
def cmd_agents(args: argparse.Namespace) -> None:
|
||||
"""Handle the 'agents' subcommand."""
|
||||
print("Supported agents:")
|
||||
for name in get_agent_names():
|
||||
info = SUPPORTED_AGENTS[name]
|
||||
print(f" {name:15s} {info['name']:20s} (hook: {info['hook_event']})")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Build the argparse parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="elydora",
|
||||
description="Elydora — tamper-evident audit trail for AI agents",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version", action="version", version="%(prog)s 1.0.0"
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# install
|
||||
install_parser = subparsers.add_parser("install", help="Install audit hook for an agent")
|
||||
install_parser.add_argument("--agent", required=True, help="Agent name (e.g. claudecode, cursor)")
|
||||
install_parser.add_argument("--org_id", required=True, help="Organization ID")
|
||||
install_parser.add_argument("--agent_id", required=True, help="Agent ID")
|
||||
install_parser.add_argument("--private_key", required=True, help="Base64url-encoded Ed25519 private key seed")
|
||||
install_parser.add_argument("--kid", required=True, help="Key ID")
|
||||
install_parser.add_argument("--token", default="", help="JWT bearer token (optional)")
|
||||
install_parser.add_argument("--base_url", default="https://api.elydora.com", help="API base URL")
|
||||
|
||||
# uninstall
|
||||
uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall audit hook for an agent")
|
||||
uninstall_parser.add_argument("--agent", required=True, help="Agent name")
|
||||
uninstall_parser.add_argument("--agent_id", default=None, help="Agent ID (if omitted, scans config files for matching agent name)")
|
||||
|
||||
# status
|
||||
subparsers.add_parser("status", help="Show installation status of all agents")
|
||||
|
||||
# agents
|
||||
subparsers.add_parser("agents", help="List supported agents")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command is None:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
handlers = {
|
||||
"install": cmd_install,
|
||||
"uninstall": cmd_uninstall,
|
||||
"status": cmd_status,
|
||||
"agents": cmd_agents,
|
||||
}
|
||||
|
||||
handler = handlers.get(args.command)
|
||||
if handler is None:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user