Skip to content

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:

json
{
  "jsonrpc": "2.0",
  "id": "unique-identifier",
  "method": "method-name",
  "params": { /* parámetros específicos */ }
}

Respuestas Exitosas

json
{
  "jsonrpc": "2.0",
  "id": "unique-identifier",
  "result": { /* resultado específico */ }
}

Respuestas de Error

json
{
  "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

json
{
  "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

json
{
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "ok": true,
    "capabilities": ["transform"],
    "plugin_version": "1.0.0"
  }
}

2. plugin.ping

Verifica la disponibilidad del plugin.

Solicitud

json
{
  "jsonrpc": "2.0",
  "id": "2",
  "method": "plugin.ping",
  "params": null
}

Respuesta

json
{
  "jsonrpc": "2.0",
  "id": "2",
  "result": {
    "pong": true
  }
}

3. file.transform

Solicita la transformación de archivos (para plugins con capacidad transform).

Solicitud

json
{
  "jsonrpc": "2.0",
  "id": "3",
  "method": "file.transform",
  "params": {
    "files": [
      {
        "path": "src/main.py",
        "sha256": "abc123...",
        "language": "python",
        "content_b64": "aW1wb3J0IG9zCg==",
        "size": 1024
      }
    ]
  }
}

Respuesta

json
{
  "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

json
{
  "jsonrpc": "2.0",
  "id": "4",
  "method": "file.analyze",
  "params": {
    "files": [
      {
        "path": "src/main.py",
        "sha256": "abc123...",
        "language": "python",
        "content_b64": "aW1wb3J0IG9zCg==",
        "size": 1024
      }
    ]
  }
}

Respuesta

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "jsonrpc": "2.0",
  "id": "6",
  "method": "plugin.shutdown",
  "params": null
}

Respuesta

json
{
  "jsonrpc": "2.0",
  "id": "6",
  "result": {
    "ok": true
  }
}

Gestión de Plugins

Comandos CLI

bash
# 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)

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)

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)

json
{
  "demo": { "nivel": "alto" },
  "decodebase64": { "mode": "aggressive", "min_length": 32 }
}

Capacidades Disponibles

CapacidadMétodoPropósito
discoverscan.discoverAñadir rutas al escaneo
transformfile.transformModificar contenido
analyzefile.analyzeAnalizar y emitir hallazgos
rulesrules.listProporcionar reglas
reportscan.reportGenerar reportes

Estructuras de Datos

PluginInit

Parámetros de inicialización del plugin.

rust
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.

rust
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.

rust
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.

rust
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

python
#!/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

python
#!/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

rust
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

bash
# 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

bash
# 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

python
#!/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

RootCause - Modular Static Analysis Engine