Skip to content

feat(runtime): route nexum:intent/pool to installed venue adapters#250

Merged
mfw78 merged 1 commit into
dev/m1from
feat/m1-pool-router
Jul 15, 2026
Merged

feat(runtime): route nexum:intent/pool to installed venue adapters#250
mfw78 merged 1 commit into
dev/m1from
feat/m1-pool-router

Conversation

@mfw78

@mfw78 mfw78 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Adds the host-side implementation of nexum:intent/pool, the strategy-facing import that routes a module's pool::submit(venue, body) call to the one installed venue adapter for that venue and drives a fixed sequence against it: quota gate, derive-header, guard interposition seam, submit.

  • host/pool_router.rs: the PoolRouter. Resolves a venue id to an installed adapter (unknown venue maps to venue-error::unknown-venue), serialises calls per adapter via a tokio async mutex held across the guest await (one venue's calls queue, different venues run in parallel; a trap projects to internal-error instead of unwinding into the caller's store), runs a GuardPolicy interposition seam on the adapter-derived header between derive-header and submit (the shipped AllowAllGuard is a no-op; PoolRouterBuilder::with_guard is the seam the egress-guard work hooks into), and enforces a per-caller submission quota (PoolQuota, default 256 charges / 60s sliding window) that also charges decode failures, so a caller feeding garbage exhausts its own budget rather than the adapter's.
  • host/impls/pool.rs, host/impls/mod.rs, host/mod.rs: wire pool in as a host binding linked into every module linker, delegating to a shared, cheaply cloneable PoolRouter carried in each store's HostState (host/state.rs).
  • bindings.rs, engine_config.rs: bindgen the pool_host world, reusing venue_adapter's type modules via with so there is no lift between two structurally identical copies of SubmitOutcome/VenueError (no new or modified WIT packages).
  • manifest/capabilities.rs: wire the pool-related capability.
  • supervisor.rs: rework around the new router plumbing.

Why

The pool world is the strategy-facing entry point for submitting intents; without a router bound host-side a module has no way to reach an installed venue adapter. Centralising venue resolution, serialisation, the guard seam and the quota in one router keeps that policy off individual adapters and gives the egress-guard work a single seam (with_guard) to hook into later.

Testing

  • cargo fmt --all -- --check
  • cargo clippy -p nexum-runtime --all-targets -- -D warnings (after cargo clean -p nexum-runtime)
  • cargo test -p nexum-runtime (229 tests pass; the concurrency/serialisation determinism test reran deterministically 5x)
  • cargo test --workspace (only nexum-runtime touched; no wasm modules or WIT packages changed, so no wasm build/clippy/bindgen-smoke gates apply)

AI Assistance

Claude (Opus) used for the implementation.

Closes #229

@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 37f8439 to c4e57ac Compare July 7, 2026 23:12
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from be70b63 to 0a3398a Compare July 7, 2026 23:12
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from c4e57ac to b13657b Compare July 8, 2026 00:42
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 0a3398a to 6f2bdbb Compare July 8, 2026 00:42
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from b13657b to 440b73a Compare July 8, 2026 01:19
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 6f2bdbb to 6806824 Compare July 8, 2026 01:19
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 440b73a to 194e8f1 Compare July 8, 2026 01:44
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 6806824 to 0e4c79b Compare July 8, 2026 01:44
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 194e8f1 to 6fb3e4d Compare July 8, 2026 02:11
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 0e4c79b to 233e9fc Compare July 8, 2026 02:11
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 6fb3e4d to f547ca7 Compare July 8, 2026 02:20
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 233e9fc to 7593566 Compare July 8, 2026 02:20
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from f547ca7 to caab1dd Compare July 8, 2026 02:23
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch 2 times, most recently from ce4648a to d074cf6 Compare July 8, 2026 03:03
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch 2 times, most recently from b4cdd00 to 2d5c02a Compare July 8, 2026 04:59
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from d074cf6 to de679c3 Compare July 8, 2026 04:59
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 2d5c02a to a2fe188 Compare July 8, 2026 05:31
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from de679c3 to 9de7450 Compare July 8, 2026 05:31
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from a2fe188 to 378311c Compare July 8, 2026 06:21
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 9de7450 to eaebfaf Compare July 8, 2026 06:21
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 378311c to c3f32b2 Compare July 8, 2026 06:40
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from eaebfaf to aa15304 Compare July 8, 2026 06:40
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from c3f32b2 to e551d21 Compare July 8, 2026 07:07
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from aa15304 to 9b7a402 Compare July 8, 2026 07:07
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from e551d21 to 2f23d71 Compare July 8, 2026 07:21
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 9b7a402 to f1f8871 Compare July 8, 2026 07:21
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 2f23d71 to a2b95dd Compare July 8, 2026 07:36
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from f1f8871 to 349d31b Compare July 8, 2026 07:36
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from e1ad254 to 560688d Compare July 8, 2026 08:51
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from cf20816 to b61ed02 Compare July 8, 2026 08:55
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 560688d to e19251f Compare July 8, 2026 08:55
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from b61ed02 to 083a41e Compare July 8, 2026 09:02
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from e19251f to d2fd2b1 Compare July 8, 2026 09:02
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 083a41e to 0eee5c3 Compare July 8, 2026 09:18
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from d2fd2b1 to be53811 Compare July 8, 2026 09:19
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 0eee5c3 to e5c2d0c Compare July 8, 2026 12:10
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from be53811 to 2cf2cf9 Compare July 8, 2026 12:10
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from e5c2d0c to 67a033b Compare July 8, 2026 12:20
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 2cf2cf9 to 24bc04f Compare July 8, 2026 12:20
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 67a033b to 058c3b6 Compare July 8, 2026 12:50
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 24bc04f to 77688fc Compare July 8, 2026 12:50
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 058c3b6 to 9b5e208 Compare July 8, 2026 13:41
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 77688fc to aa0304e Compare July 8, 2026 13:41
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 9b5e208 to 5b0c585 Compare July 8, 2026 13:59
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from aa0304e to 6db2785 Compare July 8, 2026 13:59
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 5b0c585 to e4c6020 Compare July 8, 2026 14:09
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch 2 times, most recently from f757151 to ca3b167 Compare July 8, 2026 22:33

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured router with good test coverage. Three items below.

Comment on lines +339 to +341
if let GuardVerdict::Deny(reason) = self.inner.guard.check(&ctx) {
return Err(VenueError::Denied(reason));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard-deny doesn't charge the caller, so a module that the guard blocks can call submit in a tight loop indefinitely. Each iteration passes quota_admits, acquires the adapter mutex, runs derive_header (a full WASM guest call), and then the guard denies — but quota is never consumed. The adapter stays occupied for the duration of each derive call, blocking every other caller to the same venue.

The existing decode_failures_are_charged_and_stop_re_invoking_the_adapter test shows the pattern; the guard-deny path needs the same treatment:

Suggested change
if let GuardVerdict::Deny(reason) = self.inner.guard.check(&ctx) {
return Err(VenueError::Denied(reason));
}
if let GuardVerdict::Deny(reason) = self.inner.guard.check(&ctx) {
self.charge(caller);
return Err(VenueError::Denied(reason));
}

/// router changing shape.
pub trait GuardPolicy: Send + Sync {
/// Decide whether the derived header may proceed to the adapter's submit.
fn check(&self, ctx: &GuardContext<'_>) -> GuardVerdict;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check is synchronous, but the egress-guard epic's real policy will need async calls — oracle price lookups, on-chain allowlist reads, facts-provider queries. Making the trait async now is a cheap one-line change; making it async after the egress-guard epic is underway is a breaking change under delivery pressure.

Suggested change
fn check(&self, ctx: &GuardContext<'_>) -> GuardVerdict;
fn check<'a>(&'a self, ctx: &'a GuardContext<'_>) -> futures::future::BoxFuture<'a, GuardVerdict>;

AllowAllGuard::check becomes Box::pin(async move { GuardVerdict::Allow }), and the call site awaits it.

Comment on lines +285 to +286
/// Whether `caller` has budget left in the current window. Read-only: it
/// prunes aged charges but does not record one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Read-only" is inaccurate — the function mutates the ledger: entry(...).or_default() inserts a new VecDeque for first-time callers, and prune() removes expired entries. The parenthetical even admits to pruning.

Suggested change
/// Whether `caller` has budget left in the current window. Read-only: it
/// prunes aged charges but does not record one.
/// Whether `caller` has budget left in the current window. Does not
/// record a charge — prunes expired entries to get an accurate count.

@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 76faaec to 24bf6f3 Compare July 8, 2026 23:05
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from ca3b167 to 2ef07d6 Compare July 8, 2026 23:05
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 24bf6f3 to 892cd0c Compare July 8, 2026 23:24
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 2ef07d6 to 52775b4 Compare July 8, 2026 23:24
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 892cd0c to 1f3c3f5 Compare July 8, 2026 23:29
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 52775b4 to 1d9b27a Compare July 8, 2026 23:29
@mfw78 mfw78 force-pushed the feat/m1-venue-adapter-world branch from 1f3c3f5 to a0d9541 Compare July 9, 2026 02:09
@mfw78 mfw78 force-pushed the feat/m1-pool-router branch from 1d9b27a to aaaa9e1 Compare July 9, 2026 02:09
Implement the strategy-facing pool import as a host binding linked into
every module linker, dispatching to a shared router that owns the
installed adapters.

- Bindings: a pool-host bindgen world importing nexum:intent/pool,
  reusing the intent and value-flow types from the venue-adapter
  bindings via `with`, so the outcome and error the router hands back to
  a module are the very types an adapter's submit produced.
- Router: resolve a venue id to its adapter, serialise invocation per
  adapter behind an async mutex (the wasmtime Store is not Sync),
  sequence derive-header, a no-op guard interposition seam, then submit.
  Status and cancel pass through without the header, guard, or quota.
- Quota: a per-caller sliding-window submission budget, with adapter
  decode failures charged to the caller so a module feeding garbage
  bodies exhausts its own budget rather than the adapter's fuel.
- Supervisor: adapters instantiate first and install into the router;
  modules then boot carrying the shared handle, rebuilt identically on
  restart. An init-failed adapter is loaded but not routable.
- Manifest: register nexum:intent/pool as a declarable module capability.
- Config: a [limits.quota] section resolving the per-caller budget.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-train Part of the current stacked PR train; lands in sequence, do not merge out of order.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants