Files
Elydora-Python-SDK/elydora/plugins/opencode.py
T

162 lines
5.3 KiB
Python

"""OpenCode integration using its current ESM plugin API."""
from __future__ import annotations
import glob
import json
import os
import sys
from ._file_io import write_json_atomic, write_text_atomic
from .base import AgentPlugin, InstallConfig, PluginStatus
from .hook_template import generate_hook_script
PLUGIN_DIR = os.path.join(os.path.expanduser("~"), ".config", "opencode", "plugins")
PLUGIN_FILENAME = "elydora-audit.mjs"
ELYDORA_DIR = os.path.join(os.path.expanduser("~"), ".elydora")
class OpenCodePlugin(AgentPlugin):
"""Install the Elydora guard and audit hooks for OpenCode."""
def _plugin_path(self) -> str:
return os.path.join(PLUGIN_DIR, PLUGIN_FILENAME)
@staticmethod
def _hook_path(agent_id: str) -> str:
return os.path.join(ELYDORA_DIR, agent_id, "hook.py")
def install(self, config: InstallConfig) -> None:
agent_id = config.get("agent_id", "")
guard_script_path = config.get("guard_script_path", "")
if not agent_id:
raise ValueError("agent_id is required")
if not guard_script_path:
raise ValueError("guard_script_path is required")
agent_dir = os.path.join(ELYDORA_DIR, agent_id)
os.makedirs(agent_dir, exist_ok=True)
config_data = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"token": config.get("token", ""),
"agent_name": config.get("agent_name", "opencode"),
}
write_json_atomic(
os.path.join(agent_dir, "config.json"),
config_data,
0o600,
"Elydora runtime config",
)
write_text_atomic(
os.path.join(agent_dir, "private.key"),
config.get("private_key", ""),
0o600,
"Elydora private key",
)
hook_path = self._hook_path(agent_id)
hook_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
)
write_text_atomic(
hook_path,
hook_script,
0o700,
"Elydora audit runtime",
)
write_text_atomic(
self._plugin_path(),
_generate_js_plugin(
hook_script_path=hook_path,
guard_script_path=guard_script_path,
),
0o700,
"OpenCode Elydora plugin",
)
print("Elydora hook installed for OpenCode.")
print(f" Plugin: {self._plugin_path()}")
def uninstall(self, agent_id: str = "") -> None:
try:
os.remove(self._plugin_path())
except FileNotFoundError:
pass
print("Elydora hook uninstalled from OpenCode.")
def status(self) -> PluginStatus:
plugin_path = self._plugin_path()
runtime_ready = False
for config_path in glob.glob(os.path.join(ELYDORA_DIR, "*", "config.json")):
try:
with open(config_path, "r", encoding="utf-8") as file:
config = json.load(file)
except (OSError, json.JSONDecodeError):
continue
agent_dir = os.path.dirname(config_path)
if config.get("agent_name") == "opencode":
runtime_ready = os.path.isfile(os.path.join(agent_dir, "hook.py")) and os.path.isfile(
os.path.join(agent_dir, "guard.py")
)
if runtime_ready:
break
installed = os.path.isfile(plugin_path) and runtime_ready
details = f"Plugin: {plugin_path}" if installed else "Not installed"
return PluginStatus(installed=installed, agent="opencode", details=details)
def _generate_js_plugin(*, hook_script_path: str, guard_script_path: str) -> str:
return f'''// Elydora Audit Plugin for OpenCode
// Generated by elydora-sdk — DO NOT EDIT
import {{ spawn, spawnSync }} from 'node:child_process';
const PYTHON_EXE = {_js_str(sys.executable)};
const HOOK_SCRIPT_PATH = {_js_str(hook_script_path)};
const GUARD_SCRIPT_PATH = {_js_str(guard_script_path)};
export const ElydoraAuditPlugin = async () => ({{
"tool.execute.before": async () => {{
const result = spawnSync(PYTHON_EXE, [GUARD_SCRIPT_PATH], {{
timeout: 5000,
stdio: ['pipe', 'ignore', 'pipe'],
}});
if (result.error) {{
throw new Error('Elydora guard failed: ' + result.error.message);
}}
if (result.status !== 0) {{
const message = result.stderr?.toString().trim() || 'Elydora guard blocked tool execution.';
throw new Error(message);
}}
}},
"tool.execute.after": async (input) => {{
const event = JSON.stringify({{
tool_name: input?.tool || 'unknown',
tool_input: input?.args || {{}},
session_id: input?.sessionID || 'unknown',
}});
const child = spawn(PYTHON_EXE, [HOOK_SCRIPT_PATH], {{
detached: true,
stdio: ['pipe', 'ignore', 'ignore'],
}});
child.on('error', (error) => {{
console.error('[elydora] audit hook failed:', error.message);
}});
child.stdin.end(event);
child.unref();
}},
}});
'''
def _js_str(value: str) -> str:
return json.dumps(value)