The runtime embedded in a wallet, running user-authorized wasm plugins that analyse a proposed transaction.
Why this is a separate epic from the guard
#57 bundled two products because both were called "the guard". They have different consumers, different trust models and different schedules.
The egress guard (#57) is product A: the runtime signs and broadcasts, and a policy checkpoint sits at the signed-transaction boundary. It needs a keystore (#34) and is blocked cross-repo.
Plugin analysis is product B: the wallet signs, never the runtime. A plugin reads a transaction and returns a verdict. There is no signing, no simulate backend and no venue vocabulary anywhere in it.
Only the fact-assembly and analyzer tier is shared, and it belongs here.
The shape
Two plugin classes, host-orchestrated in a pipeline:
A decoder takes a transaction and its trace and returns normalized asset movements, ERC-20, ERC-721 and ERC-1155 in and out, plus approvals.
A risk plugin consumes that and returns a verdict.
The interchange type between them is host-defined, which is what keeps decoder and risk plugin independently replaceable and keeps the host able to validate, version and budget the boundary.
Two shipped schemas are worth borrowing from rather than inventing: Tenderly's assetChanges and Alchemy's changes array, the latter carrying APPROVE as a first-class change type, which matters because approvals are where losses happen and a pure transfer diff misses them.
What is host-to-guest, and why that is the whole design
The caller is host Rust in both stages. A decoder feeding a risk plugin is cascading host-to-guest, not guest-to-guest.
That is not merely sufficient, it is better: consent does not compose. If plugins could call each other, approving one plugin would implicitly grant it whatever the plugins it calls can reach, and the user's flat consent list would become an unreviewed delegation graph. With the host as sole caller, every edge is one the wallet chose and the pipeline is auditable. It also removes the cycle problem, makes budgets chargeable to one pipeline rather than N callees, and makes failure attributable.
Constraints established by research, expensive to retrofit
Bind consent to the artifact digest, never to a publisher, a version string or a permission label. Every browser-extension supply-chain incident on record kept the identity constant and changed the bytes.
The verdict type needs an other(string) escape or a versioned envelope on day one. Component Model function subtyping is unimplemented, so adding a variant case later breaks every installed plugin.
Freeze WIT worlds, stamp the ABI version in a custom section, dispatch per artifact. Zed, Kubewarden and WASI 0.2/0.3 converged on this independently.
Scope egress per plugin in v1 of this epic or it never happens.
Verdicts are advisory by default. A third-party plugin that can veto is a censorship and denial-of-service vector.
Grant chain reads through a method allowlist with a per-invocation call budget, not a live provider handle. Blockaid's PPOM is the shipped reference: seventeen read-only methods, 300 requests per transaction.
Known gaps in the runtime today
The module set is fixed at Supervisor::boot with no install or uninstall.
There is no call-and-return path; on-trigger returns result<_, fault> and world synthesis cannot emit a typed export.
Artifacts and manifests must be files.
The import walk is not fail-closed: an unrecognized non-wasi: import passes admission and is caught only by the linker lacking a definition, which is tolerable for an operator-vetted artifact and not for one downloaded a minute ago.
The local-store namespace is keccak of the author-chosen [component].name, gated for duplicates at boot only, so two registry plugins choosing one name is ungated.
Do not build this speculatively
The embedder does not exist yet.
#251 is the cautionary tale: verification machinery for a consumer that was never wired.
Let the first real requirement shape the seam.
The runtime embedded in a wallet, running user-authorized wasm plugins that analyse a proposed transaction.
Why this is a separate epic from the guard
#57 bundled two products because both were called "the guard". They have different consumers, different trust models and different schedules.
The egress guard (#57) is product A: the runtime signs and broadcasts, and a policy checkpoint sits at the signed-transaction boundary. It needs a keystore (#34) and is blocked cross-repo.
Plugin analysis is product B: the wallet signs, never the runtime. A plugin reads a transaction and returns a verdict. There is no signing, no simulate backend and no venue vocabulary anywhere in it.
Only the fact-assembly and analyzer tier is shared, and it belongs here.
The shape
Two plugin classes, host-orchestrated in a pipeline:
A decoder takes a transaction and its trace and returns normalized asset movements, ERC-20, ERC-721 and ERC-1155 in and out, plus approvals.
A risk plugin consumes that and returns a verdict.
The interchange type between them is host-defined, which is what keeps decoder and risk plugin independently replaceable and keeps the host able to validate, version and budget the boundary.
Two shipped schemas are worth borrowing from rather than inventing: Tenderly's
assetChangesand Alchemy'schangesarray, the latter carryingAPPROVEas a first-class change type, which matters because approvals are where losses happen and a pure transfer diff misses them.What is host-to-guest, and why that is the whole design
The caller is host Rust in both stages. A decoder feeding a risk plugin is cascading host-to-guest, not guest-to-guest.
That is not merely sufficient, it is better: consent does not compose. If plugins could call each other, approving one plugin would implicitly grant it whatever the plugins it calls can reach, and the user's flat consent list would become an unreviewed delegation graph. With the host as sole caller, every edge is one the wallet chose and the pipeline is auditable. It also removes the cycle problem, makes budgets chargeable to one pipeline rather than N callees, and makes failure attributable.
Constraints established by research, expensive to retrofit
Bind consent to the artifact digest, never to a publisher, a version string or a permission label. Every browser-extension supply-chain incident on record kept the identity constant and changed the bytes.
The verdict type needs an
other(string)escape or a versioned envelope on day one. Component Model function subtyping is unimplemented, so adding a variant case later breaks every installed plugin.Freeze WIT worlds, stamp the ABI version in a custom section, dispatch per artifact. Zed, Kubewarden and WASI 0.2/0.3 converged on this independently.
Scope egress per plugin in v1 of this epic or it never happens.
Verdicts are advisory by default. A third-party plugin that can veto is a censorship and denial-of-service vector.
Grant chain reads through a method allowlist with a per-invocation call budget, not a live provider handle. Blockaid's PPOM is the shipped reference: seventeen read-only methods, 300 requests per transaction.
Known gaps in the runtime today
The module set is fixed at
Supervisor::bootwith no install or uninstall.There is no call-and-return path;
on-triggerreturnsresult<_, fault>and world synthesis cannot emit a typed export.Artifacts and manifests must be files.
The import walk is not fail-closed: an unrecognized non-
wasi:import passes admission and is caught only by the linker lacking a definition, which is tolerable for an operator-vetted artifact and not for one downloaded a minute ago.The local-store namespace is keccak of the author-chosen
[component].name, gated for duplicates at boot only, so two registry plugins choosing one name is ungated.Do not build this speculatively
The embedder does not exist yet.
#251 is the cautionary tale: verification machinery for a consumer that was never wired.
Let the first real requirement shape the seam.