Protocolo JSON-RPC para Plugins
RootCause utiliza el protocolo JSON-RPC 2.0 para la comunicación entre el host y los plugins. Esta documentación técnica detalla la implementación completa del protocolo, incluyendo todos los métodos, estructuras de datos y flujos de comunicación.
La comunicación ocurre a través del stdin
y el stdout
del proceso invocado (el plugin).
Protocolo JSON-RPC 2.0
Estructura Base de Mensajes
Todos los mensajes siguen el estándar JSON-RPC 2.0:
{
"jsonrpc": "2.0",
"id": "unique-identifier",
"method": "method-name",
"params": { /* parámetros específicos */ }
}
Respuestas Exitosas
{
"jsonrpc": "2.0",
"id": "unique-identifier",
"result": { /* resultado específico */ }
}
Respuestas de Error
{
"jsonrpc": "2.0",
"id": "unique-identifier",
"error": {
"code": -32601,
"message": "Method not found",
"data": { /* datos adicionales */ }
}
Flujo Detallado de Análisis
Métodos del Protocolo
1. plugin.init
Inicializa el plugin y establece la sesión de trabajo.
Solicitud
{
"jsonrpc": "2.0",
"id": "1",
"method": "plugin.init",
"params": {
"api_version": "1.x",
"session_id": "unique-session-id",
"workspace_root": "/path/to/workspace",
"rules_root": "/path/to/rules",
"capabilities_requested": ["transform", "analyze"],
"options": {
"mode": "aggressive",
"timeout": 5000
},
"limits": {
"cpu_ms": 10000,
"mem_mb": 64
},
"env": {
"PATH": "/usr/bin:/bin",
"LANG": "en_US.UTF-8"
}
}
}
Respuesta
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"ok": true,
"capabilities": ["transform"],
"plugin_version": "1.0.0"
}
}
2. plugin.ping
Verifica la disponibilidad del plugin.
Solicitud
{
"jsonrpc": "2.0",
"id": "2",
"method": "plugin.ping",
"params": null
}
Respuesta
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"pong": true
}
}
3. file.transform
Solicita la transformación de archivos (para plugins con capacidad transform
).
Solicitud
{
"jsonrpc": "2.0",
"id": "3",
"method": "file.transform",
"params": {
"files": [
{
"path": "src/main.py",
"sha256": "abc123...",
"language": "python",
"content_b64": "aW1wb3J0IG9zCg==",
"size": 1024
}
]
}
}
Respuesta
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"files": [
{
"path": "src/main.py",
"actions": ["decoded:base64"],
"content_b64": "aW1wb3J0IG9zCg==",
"notes": ["blocks:3"]
}
],
"metrics": {
"decoded": 1,
"ms": 150
}
}
}
4. file.analyze
Solicita el análisis de archivos (para plugins con capacidad analyze
).
Solicitud
{
"jsonrpc": "2.0",
"id": "4",
"method": "file.analyze",
"params": {
"files": [
{
"path": "src/main.py",
"sha256": "abc123...",
"language": "python",
"content_b64": "aW1wb3J0IG9zCg==",
"size": 1024
}
]
}
}
Respuesta
{
"jsonrpc": "2.0",
"id": "4",
"result": {
"findings": [
{
"message": "Hardcoded password detected",
"file": "src/main.py",
"line": 15,
"column": 10,
"severity": "high",
"rule_id": "hardcoded-password"
}
],
"metrics": {
"files_processed": 1,
"ms": 200
}
}
}
5. scan.report
Solicita la generación de reportes personalizados (para plugins con capacidad report
).
Solicitud
{
"jsonrpc": "2.0",
"id": "5",
"method": "scan.report",
"params": {
"findings": [
{
"message": "Hardcoded password detected",
"file": "src/main.py",
"line": 15,
"column": 10,
"severity": "high",
"rule_id": "hardcoded-password"
},
{
"message": "SQL injection vulnerability",
"file": "src/db.py",
"line": 42,
"column": 5,
"severity": "critical",
"rule_id": "sql-injection"
}
],
"metrics": {
"files_scanned": 150,
"total_findings": 25,
"scan_duration_ms": 5000,
"plugins_used": ["decodebase64", "ts-eval"]
}
}
}
Respuesta
{
"jsonrpc": "2.0",
"id": "5",
"result": {
"summary": {
"critical": 1,
"high": 1,
"medium": 0,
"low": 0,
"info": 0
},
"files_generated": [
{
"path": "report.json",
"size": 1024,
"format": "json"
}
],
"metrics": {
"processing_time_ms": 150
}
}
}
6. plugin.shutdown
Solicita la finalización del plugin.
Solicitud
{
"jsonrpc": "2.0",
"id": "6",
"method": "plugin.shutdown",
"params": null
}
Respuesta
{
"jsonrpc": "2.0",
"id": "6",
"result": {
"ok": true
}
}
Gestión de Plugins
Comandos CLI
# Crear plugin desde plantilla
rootcause plugin init ./mi-plugin
# Instalar plugin
rootcause plugin install ./mi-plugin
rootcause plugin install https://github.com/usuario/plugin.git
# Gestionar plugins
rootcause plugin list # Listar instalados
rootcause plugin remove nombre-plugin # Eliminar
rootcause plugin verify ./plugin # Verificar funcionamiento
# Usar plugins en análisis
rootcause ./code --rules ./rules --plugin ./plugins/decodebase64
# Múltiples plugins con opciones
rootcause ./code --rules ./rules \
--plugin decodebase64 \
--plugin ts-eval \
--plugin-opt decodebase64.mode=aggressive \
--plugin-opt ts-eval.max_lines=2000
# Configuración desde archivo
rootcause scan --plugin-config plugins.json <ruta>
Estructura de Plugin
mi-plugin/
├── plugin.toml # Manifiesto
├── plugin.py # Script principal
└── schema.json # Esquema de configuración (opcional)
Manifiesto (plugin.toml)
name = "mi-plugin"
version = "1.0.0"
api_version = "1.x"
entry = "python3 plugin.py"
capabilities = ["analyze"]
timeout_ms = 10000
needs_content = true
reads_fs = false
config_schema = "schema.json" # opcional
Esquema de Configuración (schema.json)
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["safe", "aggressive"],
"default": "safe"
},
"min_length": {
"type": "integer",
"minimum": 16,
"default": 64
}
}
}
Configuración de Plugins (plugins.json)
{
"demo": { "nivel": "alto" },
"decodebase64": { "mode": "aggressive", "min_length": 32 }
}
Capacidades Disponibles
Capacidad | Método | Propósito |
---|---|---|
discover | scan.discover | Añadir rutas al escaneo |
transform | file.transform | Modificar contenido |
analyze | file.analyze | Analizar y emitir hallazgos |
rules | rules.list | Proporcionar reglas |
report | scan.report | Generar reportes |
Estructuras de Datos
PluginInit
Parámetros de inicialización del plugin.
pub struct PluginInit {
pub api_version: String, // Versión de API esperada
pub session_id: String, // ID único de sesión
pub workspace_root: String, // Raíz del workspace
pub rules_root: String, // Raíz de reglas
pub capabilities_requested: Vec<String>, // Capacidades solicitadas
pub options: Value, // Opciones específicas
pub limits: Option<Limits>, // Límites de recursos
pub env: HashMap<String, String>, // Variables de entorno
}
PluginInitResponse
Respuesta de inicialización.
pub struct PluginInitResponse {
pub ok: bool, // Éxito de inicialización
pub capabilities: Vec<String>, // Capacidades reportadas
pub plugin_version: String, // Versión del plugin
}
FileSpec
Especificación de archivo.
pub struct FileSpec {
pub path: String, // Ruta relativa
pub sha256: Option<String>, // Hash SHA-256
pub language: Option<String>, // Lenguaje detectado
pub content_b64: Option<String>, // Contenido en base64
pub size: Option<u64>, // Tamaño en bytes
}
Limits
Límites de recursos.
pub struct Limits {
pub cpu_ms: Option<u64>, // Tiempo máximo CPU (ms)
pub mem_mb: Option<u64>, // Memoria máxima (MB)
}
Ejemplos de Implementación
Python - Plugin Básico
#!/usr/bin/env python3
import sys
import json
def send(msg_id, result=None, error=None):
payload = {"jsonrpc": "2.0", "id": msg_id}
if error is None:
payload["result"] = result
else:
payload["error"] = error
sys.stdout.write(json.dumps(payload) + "\n")
sys.stdout.flush()
for line in sys.stdin:
msg = json.loads(line)
mid = msg.get("id")
method = msg.get("method")
params = msg.get("params", {})
if method == "plugin.init":
send(mid, {"ok": True, "capabilities": ["analyze"]})
elif method == "file.analyze":
# Tu lógica de análisis aquí
send(mid, {"findings": []})
elif method == "plugin.ping":
send(mid, {"pong": True})
elif method == "plugin.shutdown":
send(mid, {"ok": True})
break
Python - Plugin de Transformación
#!/usr/bin/env python3
import sys
import json
import base64
import re
def send_response(id, result=None, error=None):
obj = {"jsonrpc": "2.0", "id": id}
obj["result" if error is None else "error"] = result if error is None else error
sys.stdout.write(json.dumps(obj) + "\n")
sys.stdout.flush()
config = {"mode": "safe", "min_length": 64}
for line in sys.stdin:
msg = json.loads(line)
mid = msg.get("id")
mth = msg.get("method")
p = msg.get("params", {})
if mth == "plugin.init":
config.update(p.get("options", {}))
send_response(mid, {
"ok": True,
"capabilities": ["transform"],
"plugin_version": "1.0.0"
})
elif mth == "file.transform":
results, decoded = [], 0
for f in p.get("files", []):
content = base64.b64decode(f.get("content_b64", "")).decode()
# Lógica de transformación aquí
results.append({
"path": f["path"],
"actions": ["decoded:base64"],
"content_b64": f["content_b64"],
"notes": ["confidence:0.92"]
})
decoded += 1
send_response(mid, {
"files": results,
"metrics": {"decoded": decoded, "ms": 0}
})
elif mth == "plugin.ping":
send_response(mid, {"pong": True})
elif mth == "plugin.shutdown":
break
Rust - Plugin Completo
use serde_json::{json, Value};
use std::io::{self, BufRead};
fn send(id: &str, result: Option<Value>, error: Option<Value>) {
let mut payload = json!({
"jsonrpc": "2.0",
"id": id
});
if let Some(result) = result {
payload["result"] = result;
} else if let Some(error) = error {
payload["error"] = error;
}
println!("{}", payload);
}
fn main() {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let msg: Value = serde_json::from_str(&line.unwrap()).unwrap();
let id = msg["id"].as_str().unwrap();
let method = msg["method"].as_str().unwrap();
let params = &msg["params"];
match method {
"plugin.init" => {
send(id, Some(json!({
"ok": true,
"capabilities": ["analyze"],
"plugin_version": "1.0.0"
})), None);
}
"file.analyze" => {
send(id, Some(json!({
"findings": []
})), None);
}
"plugin.ping" => {
send(id, Some(json!({"pong": true})), None);
}
"plugin.shutdown" => {
send(id, Some(json!({"ok": true})), None);
break;
}
_ => {
send(id, None, Some(json!({
"code": 1002,
"message": "unknown method"
})));
}
}
}
}
Testing y Verificación
Comandos de Testing
# Verificar el manifiesto y handshake
rootcause plugin verify ./mi-plugin
# Verificar con opciones específicas
rootcause plugin verify ./mi-plugin --plugin-opt mi-plugin.mode=aggressive
# Medir rendimiento con dataset de prueba
rootcause plugin bench ./mi-plugin
# Benchmark con configuración específica
rootcause plugin bench ./mi-plugin --plugin-opt mi-plugin.min_length=32
Testing Manual
# Test manual del handshake
echo '{"jsonrpc":"2.0","id":"1","method":"plugin.init","params":{"api_version":"1.3.0","capabilities_requested":["transform"]}}' | ./plugin.py
# Verificar transformación
echo '{"jsonrpc":"2.0","id":"2","method":"file.transform","params":{"files":[]}}' | ./plugin.py
Test de Funcionalidad
#!/usr/bin/env python3
import json
import sys
def test_handshake():
"""Test básico del handshake"""
init_msg = {
"jsonrpc": "2.0",
"id": "test-1",
"method": "plugin.init",
"params": {
"api_version": "1.3.0",
"capabilities_requested": ["analyze"]
}
}
# Enviar mensaje y verificar respuesta
print(json.dumps(init_msg))
# Leer respuesta
response = json.loads(sys.stdin.readline())
assert response["result"]["ok"] == True
assert "analyze" in response["result"]["capabilities"]
print("✅ Handshake test passed")
if __name__ == "__main__":
test_handshake()
Checklist de Testing
- [ ] Manifiesto válido:
plugin.toml
sin errores - [ ] Handshake exitoso: Respuesta correcta a
plugin.init
- [ ] Funcionalidad principal: Tests para capacidades declaradas
- [ ] Heartbeat: Respuesta a
plugin.ping
- [ ] Shutdown limpio: Liberación de recursos
- [ ] Manejo de errores: Respuestas apropiadas a errores