Skip to main content
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: 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: Optional without a capability: 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

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:

Payloads

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

DecodeResult, from decode

  • 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

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

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

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

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:
expect.statements compares only the fields it names. expect.error: true means decode must report an error.

Next

Writing a Plug-in

The SDKs that implement this contract for you.

Codec Plug-ins

Configure a lane with a plug-in and read how it fails.