<!-- Source: https://docs.biosimulant.com/references/desktop-plugins/runtime -->

# Runtime & JSON-RPC Protocol

Plugins run as sidecar processes that communicate with the desktop app host via **JSON-RPC 2.0 over stdin/stdout**. The host spawns your entrypoint, sends requests as newline-delimited JSON on stdin, and reads responses from stdout.

## Protocol Basics

Every message follows the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) format:

**Request (host → plugin):**
```json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "method": "plugin/health",
  "params": {}
}
```

**Response (plugin → host):**
```json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "result": {
    "ok": true,
    "plugin_id": "your-org.your-plugin",
    "version": "0.1.0"
  }
}
```

**Error response:**
```json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "error": {
    "message": "Something went wrong"
  }
}
```

Messages are delimited by newlines, one JSON object per line. Your process must read from stdin line-by-line and write responses to stdout with a trailing newline.

## Required Methods

Every plugin **must** implement these two methods:

### `plugin/health`

Health check called by the host to verify your plugin is running.

**Request params:** `{}`

**Expected response:**
```json
{
  "ok": true,
  "plugin_id": "your-org.your-plugin",
  "version": "0.1.0"
}
```

### `plugin/shutdown`

Graceful shutdown request. Your plugin should clean up resources and exit.

**Request params:** `{}`

**Expected response:**
```json
{
  "ok": true
}
```

After responding, your process should exit with code 0.

## Calling the Host

Your plugin can call host methods by sending a JSON-RPC request on stdout. The host will respond on stdin. This is the reverse direction: your plugin acts as the client.

```python
def host_call(method, params=None):
    request_id = f"host-{os.getpid()}-{method}"
    send({
        "jsonrpc": "2.0",
        "id": request_id,
        "method": method,
        "params": params or {},
    })
    # Read responses until you find the matching ID
    while True:
        message = recv()
        if str(message.get("id")) == request_id:
            if "error" in message:
                raise RuntimeError(message["error"]["message"])
            return message.get("result", {})
```

### Available Host Methods

| Method | Description |
|--------|-------------|
| `host/ping` | Verify bidirectional communication. Returns `{"pong": true}`. |
| `host/request_approval` | Request user approval for a permission. Params: `{"permission": "workspace.read"}`. Returns `{"approved": true/false}`. |

**Info:**

  Agent provider plugins have additional host methods available for session management, CLI adapter integration, and approval decisions. See the Codex plugin example for details.

## Notifications (Fire-and-Forget)

Your plugin can send notifications to the host as JSON-RPC messages without an `id` field. These are one-way and don't expect a response.

```json
{
  "jsonrpc": "2.0",
  "method": "codex/event",
  "params": {
    "kind": "notification",
    "method": "item/completed",
    "params": { "item": { "type": "agentMessage", "text": "Done!" } }
  }
}
```

The desktop app listens for these notifications to update the UI in real time (e.g., streaming agent responses).

## Message Flow Diagram

```
Host (Desktop App)                    Plugin Process
       │                                     │
       │──── plugin/health ─────────────────►│
       │◄─── {ok: true} ───────────────────│
       │                                     │
       │──── your/custom/method ───────────►│
       │                                     │
       │     (plugin calls host)             │
       │◄─── host/request_approval ─────────│
       │──── {approved: true} ─────────────►│
       │                                     │
       │◄─── result ───────────────────────│
       │                                     │
       │──── plugin/shutdown ──────────────►│
       │◄─── {ok: true} ───────────────────│
       │                              exit(0)│
```

## Threading Considerations

If your plugin handles long-running operations, you'll need to handle concurrency. The host may send new requests while you're processing a previous one.

For Python plugins, use threading to separate the stdin reader from request processing:

```python

class HostBridge:
    def __init__(self):
        self._requests = queue.Queue()
        self._pending = {}
        self._reader = threading.Thread(target=self._read_loop, daemon=True)
        self._reader.start()

    def _read_loop(self):
        while True:
            line = sys.stdin.readline()
            if not line:
                return
            message = json.loads(line)
            if "method" in message:
                # Incoming request from host
                self._requests.put(message)
            else:
                # Response to our outgoing host call
                pending = self._pending.pop(str(message.get("id")), None)
                if pending:
                    pending.put(message)

    def next_request(self):
        return self._requests.get()
```

This pattern lets your plugin make host calls (e.g., `host/request_approval`) in the middle of processing a request without deadlocking.

## Stderr

Anything your plugin writes to stderr is captured by the host as diagnostic output. Use stderr for logging. It won't interfere with the JSON-RPC protocol on stdout.

```python

def log(message):
    print(message, file=sys.stderr)
```

**Warning:**

  Never write non-JSON content to stdout. The host expects one valid JSON object per line. Debug output should go to stderr.
