Gunnar

nordisk/monetize

.nornir/02-retiring-the-linked-plugins.md

# Retiring the linked plugins — what it costs, what it buys, and what must
# replace the thing it removes

*For the owner to decide. Written 2026-09-23 by the lane that built the guests;
the decision is not ours.*

## The one-line version

Every product monetize sells now exists twice: as a Rust crate linked into the
deployment binary, and as a signed `.wasm` the open server loads behind the
same factory. The two agree — that is measured, not asserted. **The linked
crates are also the only thing that proves the guests right**, so retiring them
retires the proof. That is the whole of the decision.

## What changes when a product moves

One line in the deployment's config:

```toml
[products.holger]
endpoint = "127.0.0.1:50051"
guest = "/etc/monetize/products/holger.wasm"
guest_digest = "…"            # or guest_trust_roots = ["…"]
grants = ["grpc"]
```

`guest =` takes the id over from a linked plugin of the same name, because
registration replaces. So it is **one product at a time, and no binary
change** — the same vetra binary serves a linked gunnar and a guest holger in
one process. Rolling back is deleting the line.

Nothing above `dyn Product` moves: the ledger, the transaction, the poller, the
ladder, the price lists, the console, the purchase page. They cannot tell the
difference, which is what the factory seam was for.

## What is gained

* **The deployment binary stops linking its products.** That was the owner's
  original requirement and it is the only thing that fully meets it: the open
  server already links none, and the vetra binary stops too.
* **A product ships on its own clock.** A signed `.wasm` is replaced without
  rebuilding or restarting monetize.
* **Blast radius.** A guest runs under fuel, an epoch deadline, a memory
  ceiling and exactly the doors its block grants. A linked plugin runs with
  everything the process has. The gunnar plugin holds an ssh key; the gunnar
  guest cannot — the key stays on the host and the guest gets a header.
* **What a product may reach becomes a config line.** `grants`, `http_hosts`,
  `guest_grpc_hosts` are an allow-list an operator wrote. A linked plugin
  dials what its code says.

## What is lost

* **The parity tests, which are the proof.** 12 in gunnar's guest, 6 in
  holger's, 10 in a third product's (cut in A25 2.7), plus the live-box comparison — every one of them
  compares a guest against the crate beside it. Delete the crate and the test
  compiles against nothing. **This is the real cost of the decision.**
* **Two mocks that live in the linked crates.** `MockGunnar` and `MockHolger`
  are the appliances every test runs against, and `monetize-server`'s own tests
  use them as dev-dependencies.
* **A debugger's straight line.** A panic in a linked plugin has a Rust
  backtrace through monetize's own frames; a guest trap is a fuel or a
  `plugin_call` refusal and the guest's own stack is not in it.
* **Start cost and memory.** Each guest is a wasmtime module compiled at start,
  and a twin plane is a second instance of the same module.

## What must replace the proof

Not optional, and this is the recommendation's condition:

1. **Keep one appliance mock per product, outside the plugin crates.**
   `MockGunnar`/`MockHolger` move to the guest crates (or to a small
   `products-testing` crate) and the guests keep being driven against them.
   Without a mock there is nothing to test either build against.
2. **Promote the live-box comparison to the thing that guards the meters.**
   `products/holger/tests/the_holger_guest_against_the_live_holger_rs.rs` already reads the real
   estate with both builds and compares reading for reading. With the linked
   build gone it becomes a comparison against **holger itself** — the numbers
   the box reports versus the numbers monetize would bill — which is a stronger
   claim than the parity it replaces, and the only one still available.
3. **Freeze the last parity run.** Before the crates go, record the meter
   tables, the refusal sentences and the fold outputs as fixtures the guest is
   held to. A transcription that drifts after the original is deleted is the
   failure this whole design has been avoiding.

## Two open items, neither a blocker, both worth deciding with it

* **A restore does not survive a monetize restart — in EITHER build.** gunnar
  has no restore-status RPC, so the phase lives in memory: the linked plugin's
  own cell, and the guest's entry in the host's state door. Same lifetime, same
  hole. A restart mid-replay leaves a box that is restoring and a monetize that
  has forgotten it was. The fix is gunnar-side (a status verb) and is not a
  reason to keep or retire anything here.
* **`guest_grpc_hosts` is now a second way to say `restoring_from_twin`.**
  Naming the twin puts it on the gRPC allow-list by itself, so for gunnar the
  older key is redundant; it stays the general key for a future guest with a
  second control plane. Two ways to say one thing is how an operator writes the
  address twice and gets it wrong once.

## Recommendation

**Retire them one product at a time, holger first, and not before (1) and (3)
above are done.**

holger first because it is the smallest surface — no key, no lanes, four meters
— and because both builds have now been read against the live box and agree.
gunnar last: it has the credential, three lanes and a restore that touches a
box's whole data set.

And keep each linked crate for one full billing period after its guest goes
live, unloaded but not deleted. The thing that would send us back to it is an
invoice that disagrees, and an invoice takes a month to disagree.

## What is measured, and where

| claim | where |
|---|---|
| the guests answer every op the server calls | `crates/monetize-product-wasm/tests/` |
| gunnar's guest = gunnar's crate (12 claims) | gunnar `crates/gunnar-monetize-guest/tests/parity_with_the_linked_plugin.rs` (gunnar-live; here 2026-09-26..28, back in gunnar 2026-09-28: a `../gunnar` path in this repo made holger's builds need a gunnar checkout) |
| holger's guest = holger's crate | `products/holger/tests/parity_with_the_holger_guest.rs` (the guest is holger's `crates/holger-monetize-guest` since 2026-09-27) |
| both builds read the LIVE holger.rs identically | `products/holger/tests/the_holger_guest_against_the_live_holger_rs.rs` |
| a restore replay, both stream directions, refusals word for word | gunnar `crates/gunnar-monetize-guest/tests/the_restore_lane_runs_through_the_streaming_door.rs` (same move) |