| 1 | # Reasonix Extension Protocol v1 |
| 2 | |
| 3 | The Extension Protocol is the stable wire contract between Reasonix (the |
| 4 | **host**) and code extensions running as out-of-process **sidecars**. It is |
| 5 | how an installed plugin with a `runtime` block intercepts runtime events, |
| 6 | owns replacement strategies, contributes streaming model providers, and |
| 7 | publishes structured UI — without ever linking into the host binary. |
| 8 | |
| 9 | - Protocol ID: `reasonix.extension.v1` |
| 10 | - Machine-readable schema: `internal/extension/protocol/schema.generated.json` |
| 11 | - Method/event/limit/error index: `docs/EXTENSION_PROTOCOL.generated.md` |
| 12 | (generated, drift-checked in CI) |
| 13 | - Go SDK (implements everything below): `sdk/go` |
| 14 | |
| 15 | This document is the prose companion to the generated index. Where they |
| 16 | disagree, the generated schema wins. |
| 17 | |
| 18 | ## Transport |
| 19 | |
| 20 | - Strict JSON-RPC 2.0 over **NDJSON**: one complete JSON object per line on |
| 21 | stdin/stdout. stderr belongs to the extension for diagnostics; the host |
| 22 | captures a bounded, credential-redacted tail for errors. |
| 23 | - Frames are capped at **8 MiB** in both directions; oversized frames are a |
| 24 | connection-fatal `frame_too_large` error. |
| 25 | - Request IDs are integers. `params` must be an object. Unknown members are |
| 26 | tolerated at the frame level; DTO decoding is strict (unknown fields are |
| 27 | rejected) so typos surface immediately. |
| 28 | |
| 29 | ## Lifecycle |
| 30 | |
| 31 | 1. The host spawns the sidecar (exec form, no shell) and sends |
| 32 | `extension/initialize` first. The params carry the manifest expectation: |
| 33 | the intercepts, replaces, providers, and UI actions the host will accept. |
| 34 | For one runtime generation, the host initializes at most four sidecars in |
| 35 | parallel under one shared 30-second startup budget. |
| 36 | 2. The sidecar answers with its declaration. The host validates it: exact |
| 37 | protocol major version, and every subscription, replacement slot, |
| 38 | provider, and UI action must be a **subset of the plugin manifest**. |
| 39 | Anything beyond the manifest fails the handshake with |
| 40 | `capability_not_declared`. |
| 41 | 3. The host sends `extension/initialized`. Any extension-to-host traffic |
| 42 | before this point poisons the connection. |
| 43 | 4. Shutdown is bounded: `extension/shutdown` with a timeout, then stdin |
| 44 | closes, then the process tree is killed if the sidecar does not exit. |
| 45 | 5. Crashes: a sidecar that dies cancels all of its pending RPCs. If it owned |
| 46 | the currently selected provider or a replacement slot, the current |
| 47 | operation fails explicitly — the host never silently falls back to another |
| 48 | model or strategy. A crashed sidecar is only restarted by an idle-time |
| 49 | runtime reload. |
| 50 | |
| 51 | ## Content references |
| 52 | |
| 53 | Payload fields marked externalizable that exceed **64 KiB** are offloaded |
| 54 | into the host content store: the frame carries an `ExternalizedField` |
| 55 | descriptor (JSON pointer, content ref, byte count, SHA-256) and a `null` |
| 56 | placeholder. The peer pages the bytes back with `host/content/read` in |
| 57 | **256 KiB** chunks, verifying byte count and hash. A single content object is |
| 58 | capped at **8 MiB**. Unknown or expired refs fail with `content_ref_expired`. |
| 59 | |
| 60 | ## Interception |
| 61 | |
| 62 | Seventeen frozen hook points (see the generated index). `extension/intercept` |
| 63 | is blocking; `extension/event` is fire-and-forget observation of the same |
| 64 | points. Event delivery uses a bounded non-blocking writer queue: saturation |
| 65 | drops the observation with a warning instead of stalling the Agent. |
| 66 | |
| 67 | - Ordinary interceptors run **sequentially** in a deterministic order: |
| 68 | priority ascending (manifest `priority`, -1000..1000, default 0), then |
| 69 | plugin ID, then registration order. |
| 70 | - Decisions per call: `continue` (pass the payload along), `block` (abort |
| 71 | the operation with a user-visible reason), `replace` (substitute the |
| 72 | payload — the host re-validates it against the point's DTO and schema |
| 73 | before use), and `allow`/`deny` (only legal at `permission.decision`). |
| 74 | A full-trust `allow` overrides a host deny and is audited. |
| 75 | - Replacement **strategy slots** (`system_prompt`, `context`, |
| 76 | `provider_request`, `provider_response`, `compaction`, `session_policy`, |
| 77 | `permission`, `frontend_events`, `tool:<name>`, `provider:<ref>`) have |
| 78 | exactly one owner across all installed plugins. The chain runs first; the |
| 79 | slot owner gets the final say. A strategy owner's timeout or error always |
| 80 | fails the operation. |
| 81 | - Timeouts: input/tool/permission points default to 5s; the |
| 82 | session/context/compaction/system-prompt family to 30s; a manifest may tune |
| 83 | per-runtime up to a 60s ceiling. Optional observation-only extensions that |
| 84 | time out are warned about once and skipped; required extensions and slot |
| 85 | owners fail the operation. |
| 86 | |
| 87 | ## Streaming providers |
| 88 | |
| 89 | An extension with the `providers` capability answers |
| 90 | `extension/provider/catalog` with descriptors equivalent to host providers |
| 91 | (models, context windows, pricing, vision, reasoning, effort) — never |
| 92 | credentials. Models appear as `plugin/<plugin>/<provider>/<model>`. |
| 93 | |
| 94 | Streams follow `extension/provider/stream/open` → `stream/chunk` → |
| 95 | `stream/end`: |
| 96 | |
| 97 | - Chunks carry a 1-based contiguous sequence number; `stream/end.lastSeq` |
| 98 | freezes the terminal boundary. The host buffers out-of-order chunks, |
| 99 | drops duplicates, and fails the stream as interrupted naming the missing |
| 100 | sequence when a gap persists. |
| 101 | - Chunk types: `text`, `reasoning` (with `signature`), `tool_call_start`, |
| 102 | `tool_call_args_delta`, `tool_call`, `usage` (including cache tokens), |
| 103 | `done`, `error`. Provider errors must be redacted by the producer and are |
| 104 | defensively redacted again by the host. |
| 105 | - Cancelling the stream context sends `stream/cancel`; the sidecar must stop |
| 106 | producing chunks. |
| 107 | - The extension reads its own environment and credentials; the host never |
| 108 | sends another provider's API keys or headers. A crashed provider never |
| 109 | triggers fallback to a different model. |
| 110 | |
| 111 | ## Structured UI |
| 112 | |
| 113 | Extensions with the `ui` capability publish `status`, `card`, `form`, and |
| 114 | `notification` payloads (`host/ui/publish`) and ask questions |
| 115 | (`host/ui/request`: confirm, input, select, multiselect). Surfaces are |
| 116 | **structured only**: no HTML, CSS, JavaScript, remote scripts, arbitrary |
| 117 | frontend components, or uncontrolled URLs; Markdown renders through each |
| 118 | frontend's existing safe renderer. Every surface update carries the plugin |
| 119 | ID, surface ID, session ID, and runtime generation; stale-generation |
| 120 | updates are dropped so late results after a tab switch or reload can never |
| 121 | overwrite current state. |
| 122 | |
| 123 | Actions declared at initialize are namespaced `/<plugin>:<action>` and are |
| 124 | invoked via `extension/ui/action`; form submissions arrive via |
| 125 | `extension/ui/submit`. |
| 126 | |
| 127 | ## Errors |
| 128 | |
| 129 | Domain errors travel as JSON-RPC error code `-32000` with structured data |
| 130 | (reason, retryable, action); `protocol_error`, `unknown_method`, |
| 131 | `invalid_params`, and `internal` use the standard JSON-RPC codes. The frozen |
| 132 | reason table lives in the generated index. |
| 133 | |
| 134 | ## Stability contract |
| 135 | |
| 136 | Within major version 1, the only permitted evolutions are: new optional |
| 137 | fields, new enum values, and new methods. Existing required fields, |
| 138 | directions, limits, error reasons, and semantics never change. The canonical |
| 139 | schema and its SHA-256 hash are produced by `cmd/extension-protocol-gen`; |
| 140 | CI's `go test ./...` enforces this via the deterministic-generation test |
| 141 | (`TestGeneratedArtifactsAreDeterministicAndCommitted`), so any drift — |
| 142 | including an accidental semantic change — fails the build. |
| 143 | |
| 144 | ## Security model |
| 145 | |
| 146 | A code extension is **full trust**: it runs outside the Reasonix sandbox |
| 147 | with the unfiltered inherited environment, can read the full session and |
| 148 | environment, can bypass permissions, and can operate the machine directly. |
| 149 | Installing, updating, replacing, or `--link`ing a plugin with a `runtime` |
| 150 | block is the authorization — there is no second confirmation. Only plugins |
| 151 | installed through the plugin flow (recorded in `plugin-packages.json`) can |
| 152 | start a sidecar; project configuration can never declare one. Before any |
| 153 | sidecar diagnostics, structured UI, interceptor reasons, or provider errors |
| 154 | reach the UI, logs, or error surfaces, the host runs its credential redaction |
| 155 | pass. Ordinary provider/model content is preserved as product data. The |
| 156 | install preview, plugin details, and capability diagnostics always display the |
| 157 | FULL TRUST block for runtime plugins. |
| 158 |