Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Subprocess Protocol

Some plugins run as native subprocesses rather than Wasm components. This is used for integrations that require platform-specific binaries (e.g., antivirus scanners, package managers) that cannot be compiled to WASM.

Protocol design

Communication uses length-prefixed JSON frames over stdin/stdout:

┌─ frame 1: request ────────────────────────────────────┐
│  {"jsonrpc": "2.0", "id": 1, "method": "analyze", ...}  │
└────────────────────────────────────────────────────────┘
┌─ frame 2: response ───────────────────────────────────┐
│  {"jsonrpc": "2.0", "id": 1, "result": {...}}           │
└────────────────────────────────────────────────────────┘

Each frame is preceded by an 8-byte big-endian length header:

[0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x2B]  # 299 bytes follows

Message types

init

Sent once after process spawn to configure the plugin.

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "init",
  "params": {
    "config": {
      "rule_paths": ["/etc/arbitraitor/rules"],
      "timeout_ms": 5000
    },
    "capabilities": {
      "allow_network": false,
      "allow_filesystem_read": ["/tmp"],
      "allow_environment": []
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "version": "1.0.0",
    "ready": true,
    "max_concurrent_calls": 4
  }
}

analyze

Analyze an artifact.

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "analyze",
  "params": {
    "artifact": {
      "digest": "sha256:abc123...",
      "path": "/tmp/arbitraitor/artifact-xyz",
      "content_type": "application/x-shellscript"
    },
    "options": {
      "depth": 0,
      "parent_digest": null
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "findings": [
      {
        "id": "custom:malicious-pattern",
        "severity": "high",
        "title": "Matched malicious pattern",
        "description": "Found known bad pattern at offset 0x1a2b",
        "locations": [
          {"path": "/tmp/artifact", "offset": 6699, "line": 42}
        ]
      }
    ],
    "metadata": {
      "rules_loaded": 1847,
      "analysis_time_ms": 23
    }
  }
}

lookup

Look up an indicator in intelligence feeds.

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "lookup",
  "params": {
    "indicator": {
      "type": "url",
      "value": "https://evil.example.com/malware.exe"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "matches": [
      {
        "indicator_type": "url",
        "indicator_value": "https://evil.example.com/malware.exe",
        "source": "urlhaus",
        "confidence": "high",
        "last_seen": "2026-06-20T12:00:00Z",
        "tags": ["malware", "payload"]
      }
    ]
  }
}

shutdown

Clean plugin shutdown.

Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "shutdown",
  "params": {}
}

Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "ok": true
  }
}

Error responses

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32603,
    "message": "Analysis timed out",
    "data": {
      "stage": "pattern_matching",
      "elapsed_ms": 5000
    }
  }
}

Error codes:

CodeMeaning
-32700Parse error (malformed JSON)
-32600Invalid request (wrong method or params)
-32603Internal error (plugin crashed or timed out)
-32000Artifact not found
-32001Unsupported content type

Writing a subprocess plugin

1. Create the manifest

[plugin]
id = "my-av"
name = "My Antivirus"
version = "1.0.0"
type = "detector"

[plugin.runtime]
type = "subprocess"
path = "/usr/local/bin/my-av-scanner"
expected_digest = "sha256:abc123..."

[plugin.capabilities]
allow_network = false
allow_filesystem_read = ["/var/lib/my-av"]
allow_environment = []

2. Implement the protocol

use std::io::{self, Read, Write};

fn main() {
    let mut stdin = io::stdin().lock();
    let mut stdout = io::stdout().lock();

    loop {
        // Read frame header (8 bytes)
        let mut header = [0u8; 8];
        stdin.read_exact(&mut header).unwrap();
        let len = u64::from_be_bytes(header) as usize;

        // Read frame body
        let mut body = vec![0u8; len];
        stdin.read_exact(&mut body).unwrap();

        // Parse JSON-RPC request
        let request: serde_json::Value = serde_json::from_slice(&body).unwrap();
        let id = request["id"].as_i64().unwrap();
        let method = request["method"].as_str().unwrap();

        // Handle request
        let response = match method {
            "init" => handle_init(&request["params"]),
            "analyze" => handle_analyze(&request["params"]),
            "lookup" => handle_lookup(&request["params"]),
            "shutdown" => break handle_shutdown(&request["params"]),
            _ => error_response(id, -32600, "Unknown method"),
        };

        // Write response frame
        let json = serde_json::to_vec(&response).unwrap();
        let header = (json.len() as u64).to_be_bytes();
        stdout.write_all(&header).unwrap();
        stdout.write_all(&json).unwrap();
        stdout.flush().unwrap();
    }
}

3. Security considerations

  • Never echo untrusted input back to stdout
  • Always honor resource limits
  • Use structured output, never shell interpolation
  • Validate all input before processing