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:
[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
.wasmis 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_hostsare 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.
MockGunnarandMockHolgerare the appliances every test runs against, andmonetize-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_callrefusal 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:
- Keep one appliance mock per product, outside the plugin crates.
MockGunnar/MockHolgermove to the guest crates (or to a smallproducts-testingcrate) and the guests keep being driven against them. Without a mock there is nothing to test either build against. - Promote the live-box comparison to the thing that guards the meters.
products/holger/tests/the_holger_guest_against_the_live_holger_rs.rsalready 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. - 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_hostsis now a second way to sayrestoring_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) |