- CI warm: ccache dep, continue-on-error, 60min timeout, YAMS_DISABLE_FAISS - Faiss: YAMS_HAS_FAISS guard, Apple Clang disable, OpenMP detection - Metadata: retry constraint failures, deduplicate batch UPSERT entries - Docker: wget for Zig, gfortran, YAMS_DISABLE_FAISS=1 - CI config: free-disk-space v1.3.1, libomp-18-dev, experimental trigger - Weekly benchmarks: faiss disable, ccache, diagnostics capture - Simeon: bump to 7763a97, EntropyRouter, concept_count - CorpusStats: extraction readiness fields - Remove committed artifacts (.wrangler, .artifacts, dispatcher_external) - Fix .vscode/settings.json hardcoded path Signed-off-by: trvon <git@trevon.dev>
690 lines
16 KiB
Markdown
690 lines
16 KiB
Markdown
# YAMS External Plugin JSON-RPC Protocol Specification
|
|
|
|
**Version:** 1.0
|
|
**Date:** 2025-11-01
|
|
**Status:** Draft
|
|
**Related:** docs/PLUGINS.md, external/yams_sdk/
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Transport](#transport)
|
|
3. [Message Format](#message-format)
|
|
4. [Lifecycle Methods](#lifecycle-methods)
|
|
5. [Content Extraction Interface](#content-extraction-interface)
|
|
6. [Error Handling](#error-handling)
|
|
7. [Examples](#examples)
|
|
8. [Implementation Notes](#implementation-notes)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
External YAMS plugins communicate with the host (YAMS daemon or ExternalPluginExtractor) via **JSON-RPC 2.0** over **stdio** (newline-delimited JSON). This protocol enables:
|
|
|
|
- **Language-agnostic plugins**: Python, JavaScript, Ruby, etc.
|
|
- **Process isolation**: Plugins run in separate processes
|
|
- **Simple debugging**: Human-readable JSON messages
|
|
- **Existing tooling**: Standard JSON-RPC libraries
|
|
|
|
### Design Principles
|
|
|
|
1. **JSON-RPC 2.0 compliant**: Standard request/response/notification format
|
|
2. **Newline-delimited**: One message per line for easy parsing
|
|
3. **Synchronous**: Request-response pairs (async via batching if needed)
|
|
4. **Stateful**: Plugin maintains state across multiple requests
|
|
5. **Graceful degradation**: Missing optional methods return NotImplemented error
|
|
|
|
---
|
|
|
|
## Transport
|
|
|
|
### Stdio Communication
|
|
|
|
- **Protocol**: JSON-RPC 2.0 over stdin/stdout
|
|
- **Framing**: Newline-delimited JSON (NDJSON)
|
|
- **Encoding**: UTF-8
|
|
- **Direction**: Bidirectional (host sends requests, plugin sends responses)
|
|
|
|
### Message Flow
|
|
|
|
```
|
|
Host (C++) Plugin (Python/etc.)
|
|
─────────── ────────────────────
|
|
stdin ──────────────────────────> receives line
|
|
{"jsonrpc":"2.0",...} parses JSON
|
|
|
|
stdout <────────────────────────── sends line
|
|
{"jsonrpc":"2.0",...} response JSON
|
|
```
|
|
|
|
### Process Management
|
|
|
|
- **Spawn**: Host spawns plugin process (e.g., `python plugin.py`)
|
|
- **Handshake**: First message MUST be `handshake.manifest`
|
|
- **Init**: Second message SHOULD be `plugin.init` with config
|
|
- **Operation**: Host sends content extraction or custom RPC requests
|
|
- **Shutdown**: Host sends `plugin.shutdown`, then terminates process
|
|
- **Timeout**: Configurable per-request timeout (default: 30s for content extraction)
|
|
- **Crash handling**: Host detects broken pipe, logs error, marks plugin unhealthy
|
|
|
|
---
|
|
|
|
## Message Format
|
|
|
|
### JSON-RPC 2.0 Structure
|
|
|
|
All messages follow [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification.
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "method_name",
|
|
"params": {
|
|
"param1": "value1",
|
|
"param2": "value2"
|
|
}
|
|
}
|
|
```
|
|
|
|
- `jsonrpc`: MUST be `"2.0"`
|
|
- `id`: Unique request identifier (number or string). Omit for notifications.
|
|
- `method`: Method name (string, case-sensitive)
|
|
- `params`: Parameters object (optional, defaults to `{}`)
|
|
|
|
#### Success Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"key": "value"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Error Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"error": {
|
|
"code": -32601,
|
|
"message": "Method not found",
|
|
"data": {
|
|
"method": "unknown_method"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Standard Error Codes
|
|
|
|
Per JSON-RPC 2.0 + YAMS extensions:
|
|
|
|
| Code | Name | Meaning |
|
|
|------|------|---------|
|
|
| -32700 | Parse error | Invalid JSON |
|
|
| -32600 | Invalid Request | Missing required fields |
|
|
| -32601 | Method not found | Method does not exist |
|
|
| -32602 | Invalid params | Invalid method parameters |
|
|
| -32603 | Internal error | Plugin internal error |
|
|
| -32000 | Extraction failed | Content extraction failed (YAMS) |
|
|
| -32001 | Unsupported format | Plugin cannot handle this file type (YAMS) |
|
|
| -32002 | Timeout | Operation timed out (YAMS) |
|
|
| -32003 | Not initialized | Plugin not initialized (YAMS) |
|
|
|
|
---
|
|
|
|
## Lifecycle Methods
|
|
|
|
### 1. handshake.manifest
|
|
|
|
**Purpose**: Discover plugin capabilities
|
|
**Called**: First message after plugin spawn
|
|
**Required**: Yes
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "handshake.manifest"
|
|
}
|
|
```
|
|
|
|
#### Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"name": "yams_ghidra",
|
|
"version": "0.0.1",
|
|
"description": "Ghidra binary analysis plugin",
|
|
"author": "YAMS Contributors",
|
|
"interfaces": ["ghidra_analysis_v1", "content_extractor_v1"],
|
|
"capabilities": {
|
|
"content_extraction": {
|
|
"formats": ["application/x-executable", "application/x-sharedlib"],
|
|
"extensions": [".exe", ".dll", ".so", ".dylib", ".elf"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `name` (string, required): Plugin identifier (alphanumeric + underscore)
|
|
- `version` (string, required): Semantic version (e.g., "1.0.0")
|
|
- `description` (string, optional): Human-readable description
|
|
- `author` (string, optional): Plugin author
|
|
- `interfaces` (array of strings, required): Supported interface IDs
|
|
- `capabilities` (object, optional): Detailed capability metadata
|
|
|
|
---
|
|
|
|
### 2. plugin.init
|
|
|
|
**Purpose**: Initialize plugin with configuration
|
|
**Called**: After handshake, before any content extraction
|
|
**Required**: Yes
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"method": "plugin.init",
|
|
"params": {
|
|
"config": {
|
|
"ghidra_install": "/opt/ghidra",
|
|
"project_dir": "/tmp/yams-ghidra",
|
|
"max_functions": 100,
|
|
"timeout_sec": 30
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Params:**
|
|
- `config` (object, required): Plugin-specific configuration
|
|
|
|
#### Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"result": {
|
|
"status": "initialized",
|
|
"config_applied": {
|
|
"ghidra_install": "/opt/ghidra",
|
|
"project_dir": "/tmp/yams-ghidra-12345"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- `status` (string, required): "initialized" or "error"
|
|
- `config_applied` (object, optional): Actually applied configuration
|
|
|
|
---
|
|
|
|
### 3. plugin.health
|
|
|
|
**Purpose**: Check plugin health and readiness
|
|
**Called**: Periodically or before heavy operations
|
|
**Required**: Yes
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 3,
|
|
"method": "plugin.health"
|
|
}
|
|
```
|
|
|
|
#### Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 3,
|
|
"result": {
|
|
"status": "ok",
|
|
"started": true,
|
|
"project_dir": "/tmp/yams-ghidra-12345",
|
|
"memory_mb": 512,
|
|
"uptime_sec": 120
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- `status` (string, required): "ok" | "degraded" | "error"
|
|
- Additional fields are plugin-specific
|
|
|
|
---
|
|
|
|
### 4. plugin.shutdown
|
|
|
|
**Purpose**: Graceful plugin shutdown
|
|
**Called**: Before process termination
|
|
**Required**: Yes
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 4,
|
|
"method": "plugin.shutdown"
|
|
}
|
|
```
|
|
|
|
#### Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 4,
|
|
"result": null
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Plugin should cleanup resources (temp files, JVM, etc.)
|
|
- Host will terminate process after timeout (default: 5s)
|
|
|
|
---
|
|
|
|
## Content Extraction Interface
|
|
|
|
### content_extractor_v1
|
|
|
|
For plugins implementing `content_extractor_v1` interface.
|
|
|
|
### 5. extractor.supports
|
|
|
|
**Purpose**: Check if plugin can extract content from file type
|
|
**Called**: Before extraction to validate support
|
|
**Required**: Yes for `content_extractor_v1`
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 5,
|
|
"method": "extractor.supports",
|
|
"params": {
|
|
"mime_type": "application/x-executable",
|
|
"extension": ".elf"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Params:**
|
|
- `mime_type` (string, optional): MIME type (e.g., "application/x-executable")
|
|
- `extension` (string, optional): File extension (e.g., ".elf")
|
|
- At least one MUST be provided
|
|
|
|
#### Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 5,
|
|
"result": {
|
|
"supported": true,
|
|
"confidence": 1.0,
|
|
"method": "ghidra_decompile"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- `supported` (bool, required): True if plugin can handle this file type
|
|
- `confidence` (float, optional): Confidence level (0.0-1.0, default: 1.0)
|
|
- `method` (string, optional): Extraction method name
|
|
|
|
---
|
|
|
|
### 6. extractor.extract
|
|
|
|
**Purpose**: Extract searchable content from binary
|
|
**Called**: After validation via `supports()`
|
|
**Required**: Yes for `content_extractor_v1`
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 6,
|
|
"method": "extractor.extract",
|
|
"params": {
|
|
"source": {
|
|
"type": "path",
|
|
"path": "/tmp/yams-binary-abc123"
|
|
},
|
|
"options": {
|
|
"max_functions": 100,
|
|
"decompile": true,
|
|
"include_metadata": true,
|
|
"timeout_sec": 30
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Params:**
|
|
- `source` (object, required): Content source
|
|
- `type` (string, required): "path" or "bytes"
|
|
- `path` (string, required if type=path): Temporary file path
|
|
- `data` (string, required if type=bytes): Base64-encoded binary data
|
|
- `options` (object, optional): Extraction options
|
|
- `max_functions` (int, optional): Maximum functions to extract (default: 100)
|
|
- `decompile` (bool, optional): Include decompiled code (default: true)
|
|
- `include_metadata` (bool, optional): Include metadata (default: true)
|
|
- `timeout_sec` (int, optional): Per-function timeout (default: 10)
|
|
|
|
#### Success Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 6,
|
|
"result": {
|
|
"success": true,
|
|
"content": "Binary: /tmp/yams-binary-abc123\nType: ELF 64-bit LSB executable\nArchitecture: x86:LE:64:default\nEntry Point: 0x401000\n\nFunctions:\n---\nFunction: main\nAddress: 0x401000\nSize: 256 bytes\nDecompiled:\nint main(int argc, char** argv) {\n if (argc < 2) {\n printf(\"Usage: %s <input>\\n\", argv[0]);\n return 1;\n }\n return process_input(argv[1]);\n}\n\n---\nFunction: process_input\nAddress: 0x401100\nSize: 128 bytes\nDecompiled:\nint process_input(char* input) {\n size_t len = strlen(input);\n if (len > MAX_LEN) {\n return -1;\n }\n return validate(input, len);\n}\n",
|
|
"metadata": {
|
|
"architecture": "x86:LE:64:default",
|
|
"type": "ELF",
|
|
"functions_count": "42",
|
|
"entry_point": "0x401000",
|
|
"extraction_method": "ghidra_decompile",
|
|
"extraction_time_ms": "1234"
|
|
},
|
|
"stats": {
|
|
"functions_extracted": 42,
|
|
"functions_decompiled": 38,
|
|
"extraction_time_ms": 1234,
|
|
"decompilation_failures": 4
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- `success` (bool, required): True if extraction succeeded
|
|
- `content` (string, required if success=true): Extracted searchable text
|
|
- `metadata` (object, optional): Key-value metadata
|
|
- `stats` (object, optional): Extraction statistics
|
|
|
|
#### Error Response
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 6,
|
|
"error": {
|
|
"code": -32000,
|
|
"message": "Extraction failed: Ghidra could not analyze binary",
|
|
"data": {
|
|
"reason": "UnsupportedArchitecture",
|
|
"architecture": "ARM:BE:32:Cortex",
|
|
"partial_functions": 5
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Plugin Error Response Format
|
|
|
|
Plugins SHOULD return structured errors:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 123,
|
|
"error": {
|
|
"code": -32000,
|
|
"message": "Human-readable error message",
|
|
"data": {
|
|
"reason": "ErrorCategory",
|
|
"details": "Additional context",
|
|
"recoverable": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Common Error Scenarios
|
|
|
|
#### 1. Plugin Not Initialized
|
|
|
|
```json
|
|
{
|
|
"code": -32003,
|
|
"message": "Plugin not initialized. Call plugin.init first.",
|
|
"data": {
|
|
"reason": "NotInitialized",
|
|
"required_method": "plugin.init"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Unsupported File Type
|
|
|
|
```json
|
|
{
|
|
"code": -32001,
|
|
"message": "Unsupported file format: application/octet-stream",
|
|
"data": {
|
|
"reason": "UnsupportedFormat",
|
|
"mime_type": "application/octet-stream",
|
|
"extension": ".bin",
|
|
"supported_formats": ["application/x-executable"]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. Extraction Timeout
|
|
|
|
```json
|
|
{
|
|
"code": -32002,
|
|
"message": "Extraction timed out after 30 seconds",
|
|
"data": {
|
|
"reason": "Timeout",
|
|
"timeout_sec": 30,
|
|
"partial_extraction": true,
|
|
"functions_extracted": 15
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. External Dependency Missing
|
|
|
|
```json
|
|
{
|
|
"code": -32603,
|
|
"message": "Ghidra not found. Set GHIDRA_INSTALL_DIR or provide ghidra_install in config.",
|
|
"data": {
|
|
"reason": "DependencyMissing",
|
|
"dependency": "ghidra",
|
|
"env_var": "GHIDRA_INSTALL_DIR",
|
|
"config_key": "ghidra_install"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Example 1: Complete Binary Extraction Flow
|
|
|
|
```json
|
|
# 1. Host spawns plugin: python plugins/yams-ghidra-plugin/plugin.py
|
|
|
|
# 2. Handshake
|
|
→ {"jsonrpc":"2.0","id":1,"method":"handshake.manifest"}
|
|
← {"jsonrpc":"2.0","id":1,"result":{"name":"yams_ghidra","version":"0.0.1","interfaces":["content_extractor_v1"]}}
|
|
|
|
# 3. Initialize
|
|
→ {"jsonrpc":"2.0","id":2,"method":"plugin.init","params":{"config":{"ghidra_install":"/opt/ghidra"}}}
|
|
← {"jsonrpc":"2.0","id":2,"result":{"status":"initialized"}}
|
|
|
|
# 4. Check health
|
|
→ {"jsonrpc":"2.0","id":3,"method":"plugin.health"}
|
|
← {"jsonrpc":"2.0","id":3,"result":{"status":"ok","started":true}}
|
|
|
|
# 5. Check support
|
|
→ {"jsonrpc":"2.0","id":4,"method":"extractor.supports","params":{"mime_type":"application/x-executable","extension":".elf"}}
|
|
← {"jsonrpc":"2.0","id":4,"result":{"supported":true}}
|
|
|
|
# 6. Extract content
|
|
→ {"jsonrpc":"2.0","id":5,"method":"extractor.extract","params":{"source":{"type":"path","path":"/tmp/test.elf"},"options":{"max_functions":50}}}
|
|
← {"jsonrpc":"2.0","id":5,"result":{"success":true,"content":"Binary: /tmp/test.elf\n...","metadata":{"functions_count":"42"}}}
|
|
|
|
# 7. Shutdown
|
|
→ {"jsonrpc":"2.0","id":6,"method":"plugin.shutdown"}
|
|
← {"jsonrpc":"2.0","id":6,"result":null}
|
|
|
|
# 8. Host terminates process
|
|
```
|
|
|
|
### Example 2: Error Handling
|
|
|
|
```json
|
|
# Extract from unsupported file
|
|
→ {"jsonrpc":"2.0","id":10,"method":"extractor.extract","params":{"source":{"type":"path","path":"/tmp/test.txt"}}}
|
|
← {"jsonrpc":"2.0","id":10,"error":{"code":-32001,"message":"Unsupported format","data":{"extension":".txt"}}}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Notes
|
|
|
|
### For Plugin Developers (Python SDK)
|
|
|
|
The YAMS SDK (`yams_sdk`) provides `BasePlugin` class that handles protocol automatically:
|
|
|
|
```python
|
|
from yams_sdk import BasePlugin, rpc
|
|
|
|
class MyExtractor(BasePlugin):
|
|
def manifest(self):
|
|
return {
|
|
"name": "my_extractor",
|
|
"version": "1.0.0",
|
|
"interfaces": ["content_extractor_v1"]
|
|
}
|
|
|
|
@rpc("extractor.supports")
|
|
def supports(self, mime_type=None, extension=None):
|
|
return {"supported": extension == ".myformat"}
|
|
|
|
@rpc("extractor.extract")
|
|
def extract(self, source, options=None):
|
|
path = source["path"]
|
|
# ... extraction logic ...
|
|
return {
|
|
"success": True,
|
|
"content": extracted_text,
|
|
"metadata": {}
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
MyExtractor().run()
|
|
```
|
|
|
|
### For Host Developers (C++ ExternalPluginExtractor)
|
|
|
|
Key requirements:
|
|
1. **Process spawning**: Use `fork()`/`exec()` or `posix_spawn()`
|
|
2. **Stdio piping**: Capture stdout, send to stdin
|
|
3. **Line buffering**: Read/write complete lines (NDJSON)
|
|
4. **Timeout handling**: Per-request timeouts with `SIGTERM` fallback
|
|
5. **Error recovery**: Detect crashes, log, mark plugin unhealthy
|
|
6. **JSON parsing**: Use nlohmann/json library
|
|
7. **Thread safety**: Synchronize plugin access if multi-threaded
|
|
|
|
Example skeleton:
|
|
|
|
```cpp
|
|
class ExternalPluginExtractor {
|
|
Result<json> call(const std::string& method, const json& params) {
|
|
// Generate request ID
|
|
int id = nextRequestId_++;
|
|
|
|
// Build JSON-RPC request
|
|
json request = {
|
|
{"jsonrpc", "2.0"},
|
|
{"id", id},
|
|
{"method", method},
|
|
{"params", params}
|
|
};
|
|
|
|
// Send to plugin stdin
|
|
pluginStdin_ << request.dump() << "\n" << std::flush;
|
|
|
|
// Read response from plugin stdout (with timeout)
|
|
std::string line;
|
|
if (!readLineWithTimeout(pluginStdout_, line, timeout_)) {
|
|
return Error{ErrorCode::Timeout, "Plugin response timeout"};
|
|
}
|
|
|
|
// Parse JSON response
|
|
json response = json::parse(line);
|
|
|
|
// Check for error
|
|
if (response.contains("error")) {
|
|
return Error{
|
|
ErrorCode::ExternalError,
|
|
response["error"]["message"]
|
|
};
|
|
}
|
|
|
|
return response["result"];
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Version History
|
|
|
|
| Version | Date | Changes |
|
|
|---------|------|---------|
|
|
| 1.0 | 2025-11-01 | Initial specification |
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
|
|
- YAMS SDK: `external/yams_sdk/`
|
|
- Ghidra Plugin: `plugins/yams-ghidra-plugin/`
|
|
- Plugin Spec: `docs/spec/plugin_spec.md`
|
|
- Content Extractor C ABI: `include/yams/plugins/content_extractor_v1.h`
|