> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.hoop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Plug-in ABI Reference

> The WebAssembly contract between the Sidecar and a codec plug-in: exports, imports, manifest, payloads and failure rules

A codec plug-in is one WebAssembly module. The Sidecar (the host) instantiates it, hands it the bytes of one connection, and reads back the statements its policy evaluates. The module sees no socket, no other connection, and no host function beyond the four imports below. This page is ABI version 1; the SDKs implement it, and any language with a wasm32 target can.

***

## Memory and calls

* The module owns its linear memory and exports it as `memory`.
* The host places input by calling `alloc(len)`, writing at the returned pointer, and passing `(ptr, len)`. After the call returns, the host calls `free(ptr, len)`.
* The module returns output as one `u64`: `(ptr << 32) | len`. The host reads `len` bytes at `ptr` and calls `free(ptr, len)`. `0` means no output.
* A host import that returns bytes does the same in reverse: the host calls the module's `alloc`, writes, and returns the packed `u64`; the module frees it.
* Strings are UTF-8. JSON payloads are objects.
* Calls are serialized. The host never calls two exports of one instance at the same time, and a host import runs on the stack of the export that called it.

***

## Exports

Required:

| export | signature | purpose |
| - | - | - |
| `memory` | memory | linear memory |
| `alloc` | `(len: u32) -> u32` | allocate `len` bytes the host will write |
| `free` | `(ptr: u32, len: u32)` | release a region `alloc` returned or the host received |
| `describe` | `() -> u64` | the manifest JSON. Called once on a throwaway instance, before any other export |
| `decode` | `(conn: u32, dir: u32, ptr: u32, len: u32) -> u64` | decode bytes; returns DecodeResult JSON |

Optional, each announced by a name in the manifest's `capabilities`. The host refuses a capability without its export, and an export without its capability:

| capability | exports | signature | purpose |
| - | - | - | - |
| `deny` | `deny` | `(conn, dir, ptr, len) -> u64` | render the message at `(ptr, len)` as the protocol's error frame; bytes out |
| `filter` | `filter` | `(conn, dir, ptr, len) -> u64` | transform bytes before inspection and forwarding; bytes out. May hold a prefix and return nothing |
| `rewrite` | `enable_rewrite`, `rewrite`, `flush` | `enable_rewrite(conn)`, `rewrite(conn, ptr, len) -> u64`, `flush(conn) -> u64` | response masking; see below |
| `credential` | `take_credential` | `(conn, ptr, len) -> u64` | trade the handle under `metadata["<protocol>.credential"]` of the request statement at `(ptr, len)` for the credential it stands for; returns CredentialResult JSON |
| `content` | `content` | `(conn, ptr, len) -> u64` | render a statement for the AI analyzer; returns ContentResult JSON |

Optional without a capability:

| export | signature | purpose |
| - | - | - |
| `open` | `(conn: u32, ptr: u32, len: u32) -> u32` | a connection begins; `(ptr, len)` is the options JSON (string to string). Non-zero refuses the connection |
| `close` | `(conn: u32)` | a connection ended |
| `_initialize` | `()` | runs once per instance before anything else, when present |

`dir` is `0` for bytes from the client and `1` for bytes from the server.

`conn` identifies a connection within one instance. Under `instances: per_connection` (the default) each instance serves one connection and `conn` is always `0`. Under `instances: per_lane` one instance serves every connection of the lane, the module keys its state by `conn`, and `open` and `close` bracket each one.

***

## Imports, module `hoop`

| import | signature | purpose |
| - | - | - |
| `analyze_sql` | `(ptr, len) -> u64` | classify SQL text with the host lexer in the manifest's `sql_dialect`; returns SQLAnalysis JSON |
| `split_sql` | `(ptr, len) -> u64` | split a multi-statement text with the host lexer; returns a JSON array of strings |
| `mask` | `(cptr, clen, vptr, vlen) -> u64` | mask one cell: column name at `(cptr, clen)`, value at `(vptr, vlen)`; returns the value to forward. Legal inside `rewrite` and `flush` only; a call elsewhere traps |
| `log` | `(level: u32, ptr, len)` | a log line at level `0` debug, `1` info, `2` warn, `3` error, tagged with the lane and connection |

A module that imports anything else fails to load. The exception is `wasi_snapshot_preview1` when the manifest says `"wasi": true`. WASI then sees no filesystem, no network, no environment and no arguments; the clock, `random_get`, `fd_write` to 1 and 2 (forwarded to the log), `proc_exit` and the memory functions work.

***

## Manifest

`describe` returns it:

```json theme={"dark"}
{
  "abi": 1,
  "protocol": "x-acmewire",
  "label": "Acme Wire",
  "version": "1.3.0",
  "capabilities": ["deny", "rewrite"],
  "sql_dialect": "mysql",
  "max_reassembly": 8388608,
  "instances": "per_connection",
  "wasi": false,
  "call_timeout_ms": 2000,
  "memory_limit_pages": 1024,
  "options": [
    {"name": "compat_mode", "label": "Compat mode", "type": "enum",
     "values": ["v3", "v4"], "default": "v4", "help": "..."}
  ]
}
```

| key | required | meaning |
| - | - | - |
| `abi` | yes | `1` |
| `protocol` | yes | the protocol name: `x-` then `[a-z0-9_-]+`. The host refuses a built-in name, and the Control Plane grants `x-` protocols by the `plugins` capability alone |
| `label` | yes | the human name the listener form shows |
| `version` | no | the plug-in's own version, for the startup log |
| `capabilities` | no | the optional exports present |
| `sql_dialect` | no | `postgres`, `mysql`, `mssql`, `clickhouse`, `oracle` or `googlesql`; required when the module imports `analyze_sql` or `split_sql` |
| `max_reassembly` | no | the largest partial message the host buffers before it fails the stream; default 8 MiB |
| `instances` | no | `per_connection` (default) or `per_lane` |
| `wasi` | no | the module imports `wasi_snapshot_preview1` |
| `call_timeout_ms` | no | deadline per export call; default 2000 |
| `memory_limit_pages` | no | 64 KiB pages the instance may grow to; default 1024 (64 MiB) |
| `options` | no | per-listener settings the form renders and `open` receives; `type` is `string`, `int`, `bool` or `enum` |

***

## Payloads

Field names are the JSON names of the Sidecar's `Statement`. The host refuses unknown fields.

### DecodeResult, from `decode`

```json theme={"dark"}
{
  "statements": [
    {
      "direction": "client",
      "text": "PURGE orders WHERE age > 90d",
      "operation": "delete",
      "effects": ["delete"],
      "relations": [{"name": "orders", "access": "write"}],
      "tables": ["orders"],
      "database": "shop",
      "result": {"columns": [{"name": "id"}], "row_count": 3, "truncated": false},
      "metadata": {"x-acmewire.verb": "PURGE", "x-acmewire.seq": "17"}
    }
  ],
  "consumed": 42,
  "error": ""
}
```

* The host fills `protocol` and refuses a payload that carries it, so a module cannot impersonate another protocol.
* `direction` defaults to the call's `dir`: `client` or `server`.
* `operation` is one of `select`, `insert`, `update`, `delete`, `create`, `drop`, `alter`, `truncate`, `grant`, `revoke`, `call`, `show`, `set`, `begin`, `commit`, `rollback`, `get`, `post`, `put`, `patch`, `head`, `options`, `connect`, `trace`, `exec_line`, `env_set`, the `sftp_*` set, `ws_message`, `ws_close`, `other` or `unknown`. Anything else is a decode error. The native verb goes under `metadata["<protocol>.verb"]`, where a `metadata` rule reads it.
* The module lowercases table and relation names. The policy compares them as they arrive.
* `consumed` counts the input bytes the module finished with. An incomplete trailing message is not an error: return the statements before it and stop `consumed` at its first byte. The host keeps the rest and passes it again, prefixed to the next read, up to `max_reassembly`.
* A non-empty `error` means the bytes are malformed for this protocol. The host drops the connection and ignores `consumed` and `statements`.
* A request that carries a credential never puts it on the statement. The module keeps the value in its connection state and puts an opaque handle (a counter is enough) under `metadata["<protocol>.credential"]`. The host calls `take_credential` only on a lane with per-request identity; audit, policy and the analyzer on every other lane see the statement as `decode` returned it.

### SQLAnalysis, from `analyze_sql`

```json theme={"dark"}
{"operation": "select", "effects": ["select"],
 "relations": [{"name": "orders", "access": "read"}], "tables": ["orders"],
 "complete": true, "reason": ""}
```

`complete: false` means the scan could not finish; `operation` is then `unknown` and `reason` says why. Copy the fields onto the statement.

### CredentialResult, from `take_credential`

```json theme={"dark"}
{"credential": "Bearer eyJ...", "ok": true, "statement": {"...": "the statement with the handle removed"}}
```

The module reads the handle under `metadata["<protocol>.credential"]`, returns the credential it stands for, removes the key from `statement` and forgets the entry, so a second call with the same handle answers `ok: false`. `ok: false` also means the request carried none. With `ok: true` the module returns `statement` with neither the handle nor the credential on it; the host replaces the statement with it before policy, audit or the analyzer see it.

### ContentResult, from `content`

```json theme={"dark"}
{"text": "Protocol: x-acmewire\nOperation: delete\n\nPURGE orders ...", "cache_key": "d41d8cd9", "ok": true}
```

`ok: false` means nothing to classify. `cache_key` names the statement's shape with literals stripped, so one verdict serves its repeats; empty means never cache.

### RewriteResult, from `rewrite` and `flush`

```json theme={"dark"}
{"bytes": "<base64 of the bytes to forward>", "cells": 12, "rows": 3}
```

`bytes` may be empty: the module may hold rows until it can rebuild them. `flush` releases everything held; the host calls it when the connection ends. `cells` counts the values `mask` returned changed (compare the bytes it returned with the bytes it received); `rows` counts the rows that hold at least one. They feed the audit trail's masked counts. A non-empty `error` field fails the stream closed.

`deny` and `filter` return raw bytes, no JSON. A trap in `filter` fails the stream closed.

***

## Masking

The host calls `enable_rewrite(conn)` before any server bytes arrive, and only when the lane has a masker. The module then receives every server chunk through `rewrite` after `decode` saw it, calls `mask(column, value)` for each cell it can name, and returns rebuilt frames. Column is empty when the protocol names none. The returned value may differ in length, so the module recomputes every length prefix. The host forwards unmasked any value the module did not pass through `mask`: the module decides which bytes are data.

***

## Failure rules

Each of these fails closed, and none reaches another connection or lane:

* A trap (unreachable, out-of-bounds, stack overflow), a call past `call_timeout_ms`, or memory above `memory_limit_pages` closes the instance. The call reports an error; the host drops the connection with the deny frame when `deny` is available and a close otherwise. Under `per_lane` the lane refuses new connections until the host rebuilds the instance.
* Malformed output (not JSON, an unknown field, an `operation` outside the list, `protocol` present) is a decode error for that connection.
* A failing `describe`, a capability without its export, an export without its capability, an undeclared import, a `protocol` without the `x-` prefix, or a sha256 mismatch refuses the module at load, so the lane never starts.

***

## Conformance

`hoop start sidecar --codec-test <module.wasm> [fixtures.json...]` proves the contract against a module. Checks that need no knowledge of the protocol run on every module; the ones that need bytes run over the author's fixtures.

A fixture file holds one or more scripts:

```json theme={"dark"}
[
  {
    "name": "purge is a delete",
    "options": {"compat_mode": "v4"},
    "steps": [
      {"dir": "client", "hex": "50000000064f52444552...",
       "expect": {"consumed": 11, "statements": [{"operation": "delete", "tables": ["orders"],
                  "metadata": {"x-acmewire.verb": "PURGE"}}]}},
      {"dir": "server", "hex": "52...", "expect": {"consumed": 9, "statements": [{"result": {"row_count": 3}}]}},
      {"dir": "client", "hex": "ff00", "expect": {"error": true}}
    ]
  }
]
```

`expect.statements` compares only the fields it names. `expect.error: true` means `decode` must report an error.

| check | needs fixtures | proves |
| - | - | - |
| 1 | no | `describe` returns a valid manifest; exports and imports match it |
| 2 | no | `decode` of zero bytes consumes nothing, returns nothing, and does not fail |
| 3 | no | `deny`, when declared, returns at least one byte for a message |
| 4 | yes | every `operation` a fixture produces is in the allowed list |
| 5 | no | a trap in the module surfaces as an error, with no hang and no host panic |
| 6 | yes | each script, replayed whole, matches its expectations |
| 7 | yes | each script, replayed one byte at a time through the host's reassembly, produces the same statements |
| 8 | yes | two scripts interleaved on two connections produce, per connection, what each produces alone |

***

## Next

<CardGroup cols={2}>
  <Card title="Writing a Plug-in" icon="code" href="/docs/setup/configuration/hoop-sidecar/plugins/writing">
    The SDKs that implement this contract for you.
  </Card>

  <Card title="Codec Plug-ins" icon="puzzle-piece" href="/docs/setup/configuration/hoop-sidecar/plugins/overview">
    Configure a lane with a plug-in and read how it fails.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.