pluginabi source

← pluginabi · API reference · version 0.1.8

3 file(s), 574 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.

Public items

A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.

ItemDefinedWorked exampleCall sites
OP_INITIAL_STATE const95—106 43 45 102 108 140 … (8 total)
OP_APPLY_OP const96—106 115 35 37 78 102 … (14 total)
OP_MAKE_OP const97—106 115 102 107 142 163 … (8 total)
OP_RENDER const98—107 63 102 143 170 90 … (7 total)
OP_SNAPSHOT const99—107 90 102 108 144 174 … (10 total)
OP_LOAD_SNAPSHOT const100—107 102 145 175 180 129 … (7 total)
valid_op fn105—216 103 106 151 154
takes_arg fn114PAB-003107 108 136
request fn130PAB-00135 43 63 78 90 97 … (18 total)
req_op fn144—37 45 136
req_state_b64 fn148PAB-00138 44 79 137 48 49 … (8 total)
req_arg_b64 fn152—39 44 80 138 138
reply_ok fn162PAB-00150 82 140 141 142 143 … (11 total)
reply_err fn172—54 135 146 149 70
reply_is_ok fn182PAB-00251 55 61 62 63 83 … (17 total)
reply_out_b64 fn194PAB-00252 84 159 163 164 165 … (13 total)
reply_err_code fn198PAB-00256 181 186 78 79 102
ERR_UNKNOWN_OP const204—216 98 149 186 123 144
ERR_MALFORMED const205—214 93 94 44 121 122
ERR_PLUGIN const206—54 56 146 181 70 79
check_request fn213PAB-00391 93 94 98 134 44 … (14 total)

src/pluginabi.loft

// pluginabi — the plugin call protocol: six operations as frames on one channel.
//
// WHAT THIS IS. A plugin implements six pure operations. The host does not call them as six
// named wasm exports, and there is no per-plugin `(ptr,len)` ABI: the host drives the plugin
// over the byte channel every loft wasm already exposes (`loft_io.loft_host_input_len` /
// `loft_host_input_copy` in, `loft_host_print` out, pumped by the host).
//
// So the six operations are FRAMES on one channel, and this file is that frame vocabulary. It is
// the only thing either side needs to agree on: no ABI, no shim, no runtime.
//
// WHY CBOR AND NOT A TEXT PROTOCOL. Plugin payloads are opaque BYTES (state, ops, snapshots —
// the plugin owns their meaning), and they must survive a round trip unchanged; canonical CBOR
// carries byte strings losslessly, and one canonical encoding is what makes a frame
// deterministic.
//
// Run: `loft test`.
//
// ---------- Using it ----------
//
// One vocabulary, used from BOTH ends — the host builds what the plugin reads,
// and the same file is the only thing either side agrees on:
//
//     // host: build a request.  Payloads are opaque bytes, base64 in this API.
//     st    = bytes_to_base64([1 as u8, 2 as u8, 255 as u8]);
//     frame = pluginabi::request(pluginabi::OP_APPLY_OP, st,
//                                bytes_to_base64([0 as u8, 7 as u8]));
//
//     // plugin: read it back
//     pluginabi::req_op(frame)           // -> "apply_op"
//     pluginabi::req_state_b64(frame)    // -> st, byte for byte
//     pluginabi::check_request(frame)    // -> "" — the empty string means VALID
//
//     // plugin: answer
//     ok  = pluginabi::reply_ok(bytes_to_base64([9 as u8, 9 as u8]));
//     bad = pluginabi::reply_err(pluginabi::ERR_UNKNOWN_OP);
//     pluginabi::reply_is_ok(ok)         // -> true
//     pluginabi::reply_err_code(bad)     // -> "unknown-op"
//
// The 255 and the 0 in those payloads are the point: they are not text, they
// survive the round trip unchanged, and canonical CBOR is what makes that true.
//
// Validate before dispatching.  `check_request` and `reply_is_ok` are both
// FAIL-CLOSED — an undecodable frame is invalid rather than empty — so a host
// that skips them is guessing about a version skew instead of refusing it.
//
// Example: @PAB-004
use cbor::*;     // the published canonical-CBOR codec
use crypto::*;   // base64 <-> bytes

// ---------- CBOR helpers ----------
// Inlined rather than depending on a project-local CBOR wrapper: a published protocol library
// must stand on published deps alone.
//
// ALL PREFIXED `pa_`, and that is not cosmetic. A library's PRIVATE helpers still share a name
// space with every other library in a consumer's program, so a plain `decode_value` here collides
// with a consumer's own `decode_value` — which is exactly what happened: the collision was
// invisible in this package's own tests and in a minimal consumer, and only surfaced in a program
// that used both. A published library must not impose names a consumer cannot see coming.
fn pa_ctext(s: text) -> CborValue        { return CText { value: s }; }
fn pa_cbool(b: boolean) -> CborValue     { return CBool { value: b }; }
fn pa_cbytes(b64: text) -> CborValue { return CBytes { value: base64_to_bytes(b64) }; }
fn pa_te(k: text, v: CborValue) -> CborEntry { return CborEntry { key: CText { value: k }, value: v }; }

fn pa_decode(bytes: vector<u8>) -> CborValue { decode(bytes).value }
fn pa_decode_ok(bytes: vector<u8>) -> boolean { d = decode(bytes); return d.ok; }

// Is `b64` a payload `base64_to_bytes` reads?  It answers the empty vector for text that is
// not base64, so only `""` may decode to nothing.
fn pa_is_base64(b64: text) -> boolean { b64 == "" || len(base64_to_bytes(b64)) > 0 }

fn pa_get(m: CborValue, key: text) -> CborValue {
  match m {
    CMap { entries } => {
      for e in entries {
        match e.key { CText { value } => { if value == key { return e.value; } }, _ => {} }
      }
    },
    _ => {}
  }
  return CNull;
}
fn pa_text(m: CborValue, key: text) -> text {
  match pa_get(m, key) { CText { value } => { return "{value}"; }, _ => {} }
  return "";
}
fn pa_bytes(m: CborValue, key: text) -> text {
  match pa_get(m, key) { CBytes { value } => { return bytes_to_base64(value); }, _ => {} }
  return "";
}

// ---------- the operation vocabulary ----------
// The six, and only these six. `valid_op` is the gate: a host that receives an unknown op must
// reject rather than guess, because a plugin and host disagreeing about the vocabulary is a
// version skew, not a request to be interpreted.
pub const OP_INITIAL_STATE: text = "initial_state";
pub const OP_APPLY_OP:      text = "apply_op";
pub const OP_MAKE_OP:       text = "make_op";
pub const OP_RENDER:        text = "render";
pub const OP_SNAPSHOT:      text = "snapshot";
pub const OP_LOAD_SNAPSHOT: text = "load_snapshot";

// Is this one of the six?  THE gate on the host side: an unknown op is a version
// skew between plugin and host, and the only safe answer is to refuse rather
// than to interpret.
pub fn valid_op(op: text) -> boolean {
  return (op == OP_INITIAL_STATE) || (op == OP_APPLY_OP) || (op == OP_MAKE_OP)
      || (op == OP_RENDER) || (op == OP_SNAPSHOT) || (op == OP_LOAD_SNAPSHOT);
}

// Which operations read the `arg` slot. Recorded here rather than in each host so the two sides
// cannot drift: a host that sends `arg` for `snapshot` is confused about the contract, and a
// plugin that reads it would be reading whatever the host happened to leave there.
// Example: @PAB-003
pub fn takes_arg(op: text) -> boolean {
  return (op == OP_APPLY_OP) || (op == OP_MAKE_OP);
}

// ---------- request frames ----------
// { "op": text, "state": bytes, "arg": bytes }
//
// `state` is always present (the empty byte string for `initial_state`), so the plugin never has
// to distinguish "absent" from "empty" — an absent-vs-empty ambiguity across a wire is exactly
// the kind of thing that produces a plausible wrong answer instead of an error.
//
// ⚠ A payload that is not base64 answers an EMPTY frame, which `check_request` refuses as
// `malformed-frame`.  Handing over a raw string by mistake (`"hello"` for its base64) is
// then an error at the front door, not a state that arrives as nothing — only `""` decodes
// to the empty byte string.
// Example: @PAB-001
pub fn request(op: text, state_b64: text, arg_b64: text) -> vector<u8> {
  if !pa_is_base64(state_b64) || !pa_is_base64(arg_b64) {
    return [];
  }
  es: vector<CborEntry> = [];
  es += [ pa_te("op", pa_ctext(op)) ];
  es += [ pa_te("state", pa_cbytes(state_b64)) ];
  es += [ pa_te("arg", pa_cbytes(arg_b64)) ];
  return encode(CMap { entries: es });
}

// Read the operation out of a request frame.  Answers "" for a frame that does
// not decode, which `valid_op` then rejects — so a corrupt frame and an unknown
// op take the same path.
pub fn req_op(frame: vector<u8>) -> text        { return pa_text(pa_decode(frame), "op"); }
// The state the host is handing over, base64.  Always present — the empty byte
// string for `initial_state` — so a plugin never has to tell absent from empty.
// Example: @PAB-001
pub fn req_state_b64(frame: vector<u8>) -> text { return pa_bytes(pa_decode(frame), "state"); }
// The operation's argument, base64.  Only `apply_op` and `make_op` carry one;
// ask `takes_arg` first, because for the other four this is whatever the host
// left in the slot.
pub fn req_arg_b64(frame: vector<u8>) -> text   { return pa_bytes(pa_decode(frame), "arg"); }

// ---------- reply frames ----------
// success: { "ok": true,  "out": bytes }
// failure: { "ok": false, "err": text }
//
// A failure carries a CODE, not a message: the host shows it to a person, and a free-form string
// from the plugin would be the one channel out of the sandbox that is neither bytes-the-host-
// inspects (render) nor a value it asked for: render output stays the only rich channel.
// Example: @PAB-001
pub fn reply_ok(out_b64: text) -> vector<u8> {
  es: vector<CborEntry> = [];
  es += [ pa_te("ok", pa_cbool(true)) ];
  es += [ pa_te("out", pa_cbytes(out_b64)) ];
  return encode(CMap { entries: es });
}

// The failure reply.  `code` is a CODE and not a message: a free-form string
// would be a channel out of the sandbox that nothing inspects, and the host is
// what turns a code into words a person reads.
pub fn reply_err(code: text) -> vector<u8> {
  es: vector<CborEntry> = [];
  es += [ pa_te("ok", pa_cbool(false)) ];
  es += [ pa_te("err", pa_ctext(code)) ];
  return encode(CMap { entries: es });
}

// true only for a well-formed success reply. A frame that does not decode, or whose `ok` is not
// the boolean true, reads as failure — the fail-closed direction.
// Example: @PAB-002
pub fn reply_is_ok(frame: vector<u8>) -> boolean {
  if !pa_decode_ok(frame) { return false; }
  match pa_get(pa_decode(frame), "ok") {
    CBool { value } => { return value; },
    _ => { return false; },
  }
}

// The success payload, base64.  Only meaningful once `reply_is_ok` says yes —
// on a failure reply it answers "", which is indistinguishable from an empty
// success.
// Example: @PAB-002
pub fn reply_out_b64(frame: vector<u8>) -> text { return pa_bytes(pa_decode(frame), "out"); }
// The failure code.  The other half of the same pair: check `reply_is_ok`, then
// read exactly one of these two.
// Example: @PAB-002
pub fn reply_err_code(frame: vector<u8>) -> text { return pa_text(pa_decode(frame), "err"); }

// ---------- the error codes a host may produce ----------
// Closed set, so a host and a UI cannot drift. `ERR_UNKNOWN_OP` is the version-skew case above;
// `ERR_MALFORMED` is a frame that did not decode; `ERR_PLUGIN` is the plugin reporting it could
// not satisfy a well-formed request (which a plugin may — e.g. load_snapshot on foreign bytes).
pub const ERR_UNKNOWN_OP: text = "unknown-op";
pub const ERR_MALFORMED:  text = "malformed-frame";
pub const ERR_PLUGIN:     text = "plugin-error";

// Validate an inbound request frame the way a host must, BEFORE dispatching it: decode, then
// check the vocabulary. Returns "" when the frame is acceptable, else the error code to reply
// with. Centralised so every host does the same two checks in the same order rather than each
// re-deriving them — the contract has one front door.
// Example: @PAB-003
pub fn check_request(frame: vector<u8>) -> text {
  if !pa_decode_ok(frame) { return ERR_MALFORMED; }
  op = pa_text(pa_decode(frame), "op");
  if !valid_op(op) { return ERR_UNKNOWN_OP; }
  return "";
}

tests/pluginabi.loft

// Tests for pluginabi — the §9c.2 plugin call protocol. Run: `loft test`.
//
// The load-bearing test is the last one: a COMPLETE six-operation plugin driven end to end over
// nothing but frames. If the protocol were missing something a real plugin needs, that test could
// not be written — which is the point of writing it here rather than asserting field-by-field.
use pluginabi::*;
use cbor::*;
use crypto::*;

// Fixture helpers. The library keeps its CBOR shims PRIVATE — exporting them is not a protocol
// library's job — so a test that builds its own payloads brings its own, exactly as a real plugin
// author would.
fn t_te(k: text, v: CborValue) -> CborEntry { return CborEntry { key: CText { value: k }, value: v }; }
fn t_cuint(n: integer) -> CborValue { return CInt { value: n }; }
fn t_decode(bytes: vector<u8>) -> CborValue { d = decode(bytes); v = d.value; return v; }
fn t_decode_ok(bytes: vector<u8>) -> boolean { d = decode(bytes); return d.ok; }
fn t_uint(m: CborValue, key: text) -> integer {
  match m {
    CMap { entries } => {
      for i in 0..len(entries) {
        e = entries[i] ?? CborEntry { key: CNull, value: CNull };
        match e.key { CText { value } => {
          if value == key { match e.value { CInt { value } => { return value; }, _ => {} } }
        }, _ => {} }
      }
    },
    _ => {},
  }
  return 0;
}

fn test_request_round_trip() {
  st = bytes_to_base64([1 as u8, 2 as u8, 255 as u8]);   // binary, incl. a non-UTF-8 byte
  ar = bytes_to_base64([0 as u8, 7 as u8]);
  f = request(OP_APPLY_OP, st, ar);

  assert(req_op(f) == OP_APPLY_OP, "op round-trips");
  assert(req_state_b64(f) == st, "state round-trips byte-exactly");
  assert(req_arg_b64(f) == ar, "arg round-trips byte-exactly");

  // the empty-state case: `initial_state` sends "" rather than omitting the field, so a plugin
  // never has to tell absent from empty
  e = request(OP_INITIAL_STATE, "", "");
  assert(req_state_b64(e) == "" && req_arg_b64(e) == "", "empty state/arg survive as empty");
  assert(req_op(e) == OP_INITIAL_STATE, "op survives alongside empty payloads");
}

fn test_reply_round_trip_and_fails_closed() {
  out = bytes_to_base64([9 as u8, 9 as u8]);
  ok = reply_ok(out);
  assert(reply_is_ok(ok), "a success reply reads as ok");
  assert(reply_out_b64(ok) == out, "its payload round-trips");

  er = reply_err(ERR_PLUGIN);
  assert(!reply_is_ok(er), "a failure reply does NOT read as ok");
  assert(reply_err_code(er) == ERR_PLUGIN, "its code round-trips");

  // FAIL-CLOSED: anything that is not a well-formed success must read as failure, never as ok.
  // This is the direction that matters — a garbled frame optimistically read as success would
  // hand the host whatever bytes happened to decode.
  assert(!reply_is_ok([]), "an empty frame is not ok");
  assert(!reply_is_ok([1 as u8, 2 as u8, 3 as u8]), "garbage bytes are not ok");
  assert(!reply_is_ok(request(OP_RENDER, "", "")), "a REQUEST frame is not a success reply");
}

// Large-payload coverage (loft#651). Every other test used 2-3 byte payloads; a real `merge3`
// frame carries a whole serialized document, so large fields are the COMMON case. This asserts the
// contract holds to 300 bytes. NOTE: loft#651 (the crash where `pa_get`'s old statement-form
// `match` corrupted a heap CborValue past ~54 bytes) is CONTEXT-SENSITIVE — it fires reliably as a
// standalone program but NOT inside this test harness, so this test does not by itself distinguish
// the buggy `pa_get` from the fixed one. The reliable repro is a program (see the zero-trust repo's
// upstream/match-stmt-return-heap-repro.loft); this test guards the contract and documents intent.
fn test_large_payload_round_trip() {
  for sz in [1, 40, 53, 54, 100, 300] {
    raw: vector<u8> = []; for i in 0..sz { raw += [ ((i * 7 + 3) & 255) as u8 ]; }
    b64 = bytes_to_base64(raw);
    // request: state AND arg both large; both must survive the read path (arg is the LAST field).
    f = request(OP_APPLY_OP, b64, b64);
    assert(req_state_b64(f) == b64, "large state round-trips byte-exactly (sz={sz})");
    assert(req_arg_b64(f) == b64, "large arg round-trips byte-exactly (sz={sz})");
    // reply: a large `out` must survive reply_out_b64, and reply_is_ok must not choke on it.
    r = reply_ok(b64);
    assert(reply_is_ok(r), "a large success reply reads as ok (sz={sz})");
    assert(reply_out_b64(r) == b64, "large reply out round-trips byte-exactly (sz={sz})");
  }
  println("plugins: pluginabi — large frame payloads (to 300 bytes) round-trip byte-exactly");
}

fn test_check_request_is_the_front_door() {
  good = request(OP_SNAPSHOT, "", "");
  assert(check_request(good) == "", "a well-formed known op passes");

  assert(check_request([7 as u8, 7 as u8]) == ERR_MALFORMED, "undecodable -> malformed");
  assert(check_request([]) == ERR_MALFORMED, "empty -> malformed");

  // version skew: an op this host does not know must be REJECTED, not guessed at
  unknown = request("apply_ops", "", "");            // note the typo/newer name
  assert(check_request(unknown) == ERR_UNKNOWN_OP, "an unknown op is rejected, not interpreted");

  // the vocabulary is exactly six
  n = 0;
  for o in [OP_INITIAL_STATE, OP_APPLY_OP, OP_MAKE_OP, OP_RENDER, OP_SNAPSHOT, OP_LOAD_SNAPSHOT] {
    if valid_op(o) { n = n + 1; }
  }
  assert(n == 6, "all six operations are in the vocabulary");
  assert(!valid_op("") && !valid_op("eval") && !valid_op("exec"), "nothing else is");
  assert(takes_arg(OP_APPLY_OP) && takes_arg(OP_MAKE_OP), "apply_op/make_op read arg");
  assert(!takes_arg(OP_SNAPSHOT) && !takes_arg(OP_INITIAL_STATE), "the others do not");
}

// ---------- a complete plugin, driven only by frames ----------
// A counter: state is CBOR { "n": uint }; an op is CBOR { "d": uint } (a delta). The plugin owns
// that encoding — the protocol only carries bytes, which is the §9c.2 contract.
//
// ⚠ This is the package's best worked example, which is why it is TAGGED rather than duplicated:
// what a protocol library owes a reader is not a signature but the SHAPE OF A WHOLE EXCHANGE —
// front door, dispatch, the six operations, a plugin refusing a well-formed request, and a host
// rejecting an op the plugin body never sees.
fn plug_state(n: integer) -> text {
  es: vector<CborEntry> = []; es += [ t_te("n", t_cuint(n)) ];
  return bytes_to_base64(encode(CMap { entries: es }));
}
fn plug_n(state_b64: text) -> integer {
  if state_b64 == "" { return 0; }
  return t_uint(t_decode(base64_to_bytes(state_b64)), "n");
}
fn plug_delta(arg_b64: text) -> integer {
  if arg_b64 == "" { return 0; }
  return t_uint(t_decode(base64_to_bytes(arg_b64)), "d");
}

// THE PLUGIN: one entry point, frame in -> frame out. This is what the wasm pump calls.
fn plugin_dispatch(frame: vector<u8>) -> vector<u8> {
  bad = check_request(frame);
  if bad != "" { return reply_err(bad); }
  op = req_op(frame);
  st = req_state_b64(frame);
  ar = req_arg_b64(frame);

  if op == OP_INITIAL_STATE { return reply_ok(plug_state(0)); }
  if op == OP_APPLY_OP      { return reply_ok(plug_state(plug_n(st) + plug_delta(ar))); }
  if op == OP_MAKE_OP       { return reply_ok(ar); }
  if op == OP_RENDER        { return reply_ok(base64_encode("count={plug_n(st)}")); }
  if op == OP_SNAPSHOT      { return reply_ok(st); }
  if op == OP_LOAD_SNAPSHOT {
    if !t_decode_ok(base64_to_bytes(st)) { return reply_err(ERR_PLUGIN); }   // foreign bytes
    return reply_ok(st);
  }
  return reply_err(ERR_UNKNOWN_OP);
}

fn call(op: text, st: text, ar: text) -> vector<u8> { return plugin_dispatch(request(op, st, ar)); }

// @PAB-004 — a whole plugin driven end to end over nothing but frames
fn test_a_whole_plugin_over_frames() {
  // initial_state
  r = call(OP_INITIAL_STATE, "", "");
  assert(reply_is_ok(r), "initial_state succeeds");
  st = reply_out_b64(r);
  assert(plug_n(st) == 0, "a fresh state counts zero");

  // make_op then apply_op, twice — the host never touches the plugin's encoding
  d5 = reply_out_b64(call(OP_MAKE_OP, st, plug_delta_arg(5)));
  st = reply_out_b64(call(OP_APPLY_OP, st, d5));
  d7 = reply_out_b64(call(OP_MAKE_OP, st, plug_delta_arg(7)));
  st = reply_out_b64(call(OP_APPLY_OP, st, d7));
  assert(plug_n(st) == 12, "5 + 7 folded through frames = 12");

  // render — the host-visible channel
  rr = call(OP_RENDER, st, "");
  assert(reply_is_ok(rr) && base64_decode(reply_out_b64(rr)) == "count=12", "render reflects state");

  // snapshot -> load_snapshot round-trips to the SAME state (the §9c.1 rollover contract)
  snap = reply_out_b64(call(OP_SNAPSHOT, st, ""));
  back = call(OP_LOAD_SNAPSHOT, snap, "");
  assert(reply_is_ok(back), "a snapshot loads");
  assert(plug_n(reply_out_b64(back)) == 12, "and restores the same count");

  // a plugin may refuse a well-formed request — foreign snapshot bytes
  bad = call(OP_LOAD_SNAPSHOT, bytes_to_base64([200 as u8, 201 as u8]), "");
  assert(!reply_is_ok(bad) && reply_err_code(bad) == ERR_PLUGIN,
    "load_snapshot on foreign bytes is a plugin error, not a protocol error");

  // and the host rejects an op the plugin never sees
  sk = plugin_dispatch(request("teleport", st, ""));
  assert(!reply_is_ok(sk) && reply_err_code(sk) == ERR_UNKNOWN_OP, "an unknown op never reaches the plugin body");

  println("plugins: pluginabi — §9c.2 frame protocol + a whole plugin driven over frames OK");
}

fn plug_delta_arg(d: integer) -> text {
  es: vector<CborEntry> = []; es += [ t_te("d", t_cuint(d)) ];
  return bytes_to_base64(encode(CMap { entries: es }));
}

tests/worked-examples.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// One test per non-obvious contract, and each one is that contract's
// worked EXAMPLE.
//
// ⚠⚠ **The pair is the deliverable.**  A snippet in a doc comment rots
// silently — nothing compiles it, so it goes stale at the first
// signature change and the reader finds out.  A test nobody links to is
// correct and invisible.  So every function below carries an
// `// Example: @PAB-0NN` line pointing here, and every tag here is
// reachable from the functions it demonstrates.
//
// ⚠ These are examples FIRST and assertions second: each is written the
// way a host or a plugin author would actually write the call.
// `tests/pluginabi.loft` already pins that the frames round-trip; what
// these pin is the shape of a CORRECT CALL — and `@PAB-004` is that
// file's whole-plugin test, which is the best example this package has.
//
// Why this library owes them: **a frame is `vector<u8>` and every
// payload is `text`.**  The signatures say bytes-in / bytes-out and
// text-in / text-out, and never that the text must be BASE64, that the
// `text` a reply hands back is `""` for two opposite outcomes, or that
// the front door checks the envelope and not the letter.  Each is a
// well-typed call that answers something other than it appears to.
use pluginabi::*;
use cbor::*;
use crypto::*;

// Sending a payload.
//
// ⚠⚠ **Every payload crossing this API is BASE64 TEXT, not the bytes and
// not the string.**  The frame is `vector<u8>`, so the base64 layer is
// invisible at the call site — so `request` checks it: a raw string that is
// not base64 answers an empty frame, which the front door refuses.
// @PAB-001 — a payload is base64 text, and a raw string is refused at the front door
fn test_a_payload_is_base64_text() {
  // ⚠ The mistake, spelled the way it types.  "hello" is five characters and
  // not base64 — `base64_to_bytes` answers nothing for it — so `request`
  // answers an empty frame, and the front door refuses it as malformed.
  pb_wrong = request(OP_APPLY_OP, "hello", "");
  assert(len(base64_to_bytes("hello")) == 0, "a raw string is not base64");
  assert(len(pb_wrong) == 0, "so there is no frame to send");
  assert(check_request(pb_wrong) == ERR_MALFORMED, "and the front door says so");

  // The correct spelling — encode at the boundary, decode at the far end:
  pb_right = request(OP_APPLY_OP, base64_encode("hello"), "");
  assert(req_state_b64(pb_right) == "aGVsbG8=", "base64 on the wire");
  assert(base64_decode(req_state_b64(pb_right)) == "hello", "and back out again");

  // For real binary state — which is the common case, since a plugin owns
  // its own encoding — `bytes_to_base64` is the door, and it survives a
  // byte no UTF-8 reader could carry.
  pb_raw: vector<u8> = [0 as u8, 200 as u8, 255 as u8];
  pb_bin = request(OP_APPLY_OP, bytes_to_base64(pb_raw), "");
  assert(len(base64_to_bytes(req_state_b64(pb_bin))) == 3, "three bytes in, three out");
  assert(bytes_to_base64(pb_raw) == req_state_b64(pb_bin), "byte-exact");
}

// Reading a reply.
//
// ⚠⚠ **`reply_is_ok` is the only function that classifies a reply.**
// `reply_out_b64` answers `""` for a failure — the same `""` a success
// carrying an empty payload answers — and `reply_err_code` answers `""`
// for a success, the same `""` an error code would have to be to be
// absent.  So neither accessor can be read as a verdict, in either
// direction.
// @PAB-002 — ask reply_is_ok first; "" is both outcomes' answer
fn test_only_reply_is_ok_classifies_a_reply() {
  rp_fail = reply_err(ERR_PLUGIN);
  rp_empty = reply_ok("");            // a genuine success with nothing to return

  // ⚠ The two payload reads are indistinguishable:
  assert(reply_out_b64(rp_fail) == "", "a failure has no out");
  assert(reply_out_b64(rp_empty) == "", "and neither has an empty success");

  // ⚠ ...and so are the two code reads, the other way round:
  assert(reply_err_code(rp_empty) == "", "a success has no code");
  assert(reply_err_code(rp_fail) == ERR_PLUGIN, "the failure's code is there");

  // The verdict, and the only one:
  assert(!reply_is_ok(rp_fail), "the failure is a failure");
  assert(reply_is_ok(rp_empty), "the empty success is a success");

  // It fails CLOSED, which is the direction that matters: a truncated or
  // garbled frame, or one sent in the wrong direction, is never read as
  // a success carrying whatever bytes happened to decode.
  assert(!reply_is_ok([]), "an empty frame is not a success");
  assert(!reply_is_ok([1 as u8, 2 as u8, 3 as u8]), "garbage is not a success");
  assert(!reply_is_ok(request(OP_RENDER, "", "")), "a REQUEST is not a success reply");

  // So the host's read order is fixed: verdict, then payload.
  assert(host_reads(rp_empty) == "ok:", "an empty success reads {host_reads(rp_empty)}");
  assert(host_reads(rp_fail) == "err:plugin-error", "a failure reads {host_reads(rp_fail)}");
  assert(host_reads([9 as u8]) == "err:", "garbage reads {host_reads([9 as u8])}");
}

// The read order the example above recommends: classify, then read the
// one field that outcome has.
fn host_reads(hr_frame: vector<u8>) -> text {
  if reply_is_ok(hr_frame) { return "ok:{reply_out_b64(hr_frame)}"; }
  return "err:{reply_err_code(hr_frame)}";
}

// Admitting an inbound request.
//
// ⚠⚠ **`check_request` checks the ENVELOPE, and `""` is its pass.**  It
// decodes the frame and looks the op up in the vocabulary — nothing
// else.  A known op carrying payload bytes the plugin cannot use passes;
// an `arg` present for an operation that does not take one passes; and a
// REPLY frame handed to it comes back `unknown-op`, because a reply
// decodes perfectly and simply has no `op`.
// @PAB-003 — the front door validates the frame, never the payload
fn test_the_front_door_checks_the_envelope() {
  // ⚠ `""` means ACCEPTABLE.  The return is the code to reply WITH, so
  // the check reads backwards from every boolean guard around it.
  fd_good = request(OP_SNAPSHOT, base64_encode("state"), "");
  assert(check_request(fd_good) == "", "a well-formed known op passes");

  // What it does catch, and the two codes it distinguishes:
  assert(check_request([7 as u8, 7 as u8]) == ERR_MALFORMED, "undecodable is malformed");
  assert(check_request([]) == ERR_MALFORMED, "so is empty");
  assert(check_request(request("apply_ops", "", "")) == ERR_UNKNOWN_OP,
         "a near-miss op is version skew, not a request to interpret");

  // ⚠ What it does NOT catch — a known op whose state is bytes no plugin
  // could load.  That is the PLUGIN's answer to give, and `ERR_PLUGIN`
  // is how it says so; the front door has no opinion.
  fd_foreign = request(OP_LOAD_SNAPSHOT, bytes_to_base64([200 as u8, 201 as u8]), "");
  assert(check_request(fd_foreign) == "", "foreign payload bytes are admitted");

  // ⚠ Nor an `arg` sent for an operation that reads none.  `takes_arg`
  // records which four do not, so the two sides cannot drift — but it is
  // a contract both must consult, not a gate either one enforces.
  fd_extra = request(OP_SNAPSHOT, base64_encode("s"), base64_encode("a"));
  assert(!takes_arg(OP_SNAPSHOT), "snapshot reads no arg");
  assert(check_request(fd_extra) == "", "yet a frame carrying one is admitted");
  assert(req_arg_b64(fd_extra) == base64_encode("a"), "and the arg is sitting there, readable");

  // ⚠ And the honest limit: a REPLY frame is well-formed CBOR with no
  // `op`, so the missing key reports as an unknown operation.  A host
  // chasing `unknown-op` is being pointed at version skew when the real
  // fault is a frame going the wrong way.
  assert(check_request(reply_ok(base64_encode("x"))) == ERR_UNKNOWN_OP,
         "a reply frame at the request door reports {check_request(reply_ok(base64_encode(\"x\")))}");

  // The vocabulary is exactly six, and nothing else is in it.
  fd_n = 0;
  for fd_o in [OP_INITIAL_STATE, OP_APPLY_OP, OP_MAKE_OP,
               OP_RENDER, OP_SNAPSHOT, OP_LOAD_SNAPSHOT] {
    if valid_op(fd_o) { fd_n = fd_n + 1; }
  }
  assert(fd_n == 6, "all six are in the vocabulary");
  assert(!valid_op("") && !valid_op("eval"), "and nothing else is");
}

fn main() {
  test_a_payload_is_base64_text();
  test_only_reply_is_ok_classifies_a_reply();
  test_the_front_door_checks_the_envelope();
  println("pluginabi worked examples passed");
}