Skip to content

chore(deps)!: migrate to nexum-runtime fd16db7 and videre 1da06af, cow-venue becomes native Rust - #675

Merged
mfw78 merged 15 commits into
mainfrom
chore/nexum-videre-migration
Aug 31, 2026
Merged

mfw78 merged 15 commits into
mainfrom
chore/nexum-videre-migration

Conversation

@mfw78

@mfw78 mfw78 commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Migrates shepherd to nexum-runtime fd16db7 and videre 1da06af, so ccow-monitor can ship.

Why this is a rewrite and not a pin bump

Upstream deleted the extension-installed component path. ProviderKind, HostService, ProviderInstance, build_provider_linker, ActorSlot, SupervisedStore and the runtime's Liveness are gone, so an extension can no longer install or supervise a guest component. A venue is now a native Rust adapter that the composition root registers in code.

What changed

crates/cow-venue is a native videre_host::VenueInvoker rather than a wasm guest. It drops crate-type = ["lib", "cdylib"], wit-bindgen, the #[videre_sdk::venue] attributes and its three module*.toml manifests. Its feature is renamed adapter to venue. The published codec vectors and header goldens are unchanged, which is the evidence the wire behaviour did not move.

crates/composable-cow and both keeper modules move from the Watch vocabulary to Commitment. Manifests become component.toml with [component], [dependencies] and [[trigger]].

crates/shepherd-engine gains a lib target so the composition root is testable, and registers the CoW venue in Rust. The eight operator configs lose their [[adapters]] tables and gain an [extensions.videre.venues.cow] section. A new tests/operator_configs.rs parses all eight through the real EngineConfig parser, so schema drift fails in CI rather than at an operator's boot.

Capability losses, recorded rather than papered over

Per-venue HTTP egress confinement is gone. http_allow = ["api.cow.fi"] fed the wasi:http gate, which only reaches guest components. A native venue owns its own client, so nothing bounds its egress. Mitigated in cow-venue by refusing redirects, so the reachable host is exactly what orderbook_url names and a 307 cannot re-send a signed order elsewhere. [policy].http_deny still scopes guests but does not reach a venue.

Operator-swappable venues are gone. Swapping a venue now means a new binary.

Nothing marks a venue dead. register returns a Liveness flag, but no path outside videre's own tests sets it, so a wedged native venue cannot be quarantined and the registry keeps routing to it.

Venue response bodies have no byte cap. The wasm memory limit used to bound them; they are now bounded only by the request timeout.

Decisions taken

The keeper store prefix changed from watch: to commitment:. No migration is written, and none is needed: there are no legacy rows in any live store. Confirmed by the operator.

Both event triggers now set resume = true, and so does ethflow-watcher's. A trigger without it re-opens at head, so every log mined during downtime was lost silently and permanently. Both keepers build their whole state from logs, so a restart at head loses any order registered while the engine was down. max_lookback stays unset on all three, because a cap drops the oldest missed blocks, which is the loss being fixed. A first boot has no cursor and still starts at head, so this backfills a restart gap only.

Replay is already idempotent and tested: persist_commitment overwrites in place keeping the newest stamp, and ethflow-watcher short-circuits on observed:{uid}. The two twap-monitor cursors are independent, so a backfilled removal can land before its own create and briefly resurrect a commitment; the next poll reverts SingleOrderNotAuthed, which LegacyRevertAdapter::classify folds to Invalid, and the commitment drops.

Chain-log durability

Two keys keep the keepers from going silently blind, and this PR sets both on every event trigger.

resume = true persists a cursor, so a restart backfills the downtime gap instead of re-opening at head.

start_block seeds the first boot, which has no cursor to resume from. Without it a fresh daemon never learns of anything that happened before it ran, and a ComposableCoW conditional order can stay live for weeks. Blocks resolved from each deploy transaction:

Module Contract Chain Block
twap-monitor ComposableCoW Sepolia 5072748
ethflow-watcher CoWSwapEthFlow Sepolia 7541028

ComposableCoW is one CREATE2 address on every chain but a different block per chain, so mainnet is 17883049 when shepherd#657 makes that move. EthFlow has had several per-network deployments, so its block belongs to the pinned address and must be re-derived if that address changes.

The seed applies only while no cursor is stored. After the first committed chunk the store wins, so it is a one-time floor and not a rescan point. max_lookback stays unset, because a cap drops the oldest missed blocks, which is the loss being fixed.

crates/shepherd-engine/tests/module_manifests.rs pins both keys on every shipped event trigger. Dropping either still parses, still boots and still passes every other test in the repo, so nothing else would catch the regression.

Pin bumps

nexum-runtime fd16db7 to 2ed882f, which is where the start_block key comes from (nullislabs/nexum-runtime#381).

videre 66c8c3b to fd8af02, its matching bump (nullislabs/videre-nexum-module#87).

These move in lock-step by necessity: both repos pin the same nexum-runtime rev, and a split pin resolves two copies of the crate whose Extension seam types do not match.

Verification

just build-modules, just fmt, just lint and just test all clean: 171 tests pass, 1 skipped.

AI Assistance: Claude Code used for the migration across five parallel units and the reconciliation.

mfw78 added 15 commits August 31, 2026 02:07
nexum-runtime to fd16db7 and videre to 1da06af, with wit/deps/
re-vendored from both. The vendored nexum:host loses messaging,
identity, remote-store and query-module, and gains trigger-module.

This commit alone does not compile: it is the base the migration
units build on.

AI Assistance: Claude Code used for the pin bump and re-vendor.
Rename `module.toml` to `component.toml` and rewrite it against the new
manifest model: `[component]`, a table-valued `[dependencies]` map, and
`[[trigger]] on =` rows. Drop the retired `kind`, `optional`, and the
placeholder `component` digest, which the runtime now verifies against
the loaded bytes.

Fold `on_chain_logs` into `on_event`, which takes one log and reads the
chain id off the WIT record before the alloy conversion drops it. Move
`nexum_sdk::events` to `nexum_sdk::sol_events` and `ChainLogParts` to
`LogParts`.

AI Assistance: Claude Code used for the API survey and the migration edits
Follow nexum-runtime fd16db7, which renamed the keeper watch vocabulary
to commitment: `WatchSet` to `CommitmentSet`, `WatchRef` to
`CommitmentRef`, and the `watch` parameter on `Gates::*`,
`Poller::poll` and `Retrier::apply` to `commitment`. Rename the run
composition's locals, log lines and doc wording to match, and carry the
same pass through the run acceptance tests.

Add the new `LocalStoreHost::list_entries` verb to the `FlakyCommit`
test double. It delegates to the inner store rather than taking the
trait default, so the mock keeps its paging behaviour; only the write
path still injects the commit fault.

BREAKING CHANGE: the keeper store prefix moves from `watch:` to
`commitment:`. An existing store's `watch:` rows are never read and
never deleted after the upgrade, and their paired `next_block:`,
`next_epoch:` and `refused:` keys are orphaned. The `submitted:` and
`observed:` journal prefixes are unchanged. No migration ships here.

AI Assistance: Claude Code used for the rename pass and the test updates.
Port the keeper to nexum-runtime fd16db7 and videre 1da06af.

Rename `module.toml` to `component.toml` and rewrite it to the new
schema: `[component]` replaces `[module]`, `[dependencies]` replaces
`[capabilities] required = [...]`, and each `[[trigger]] on =` replaces
a `[[subscription]] kind =`. The two `chain-log` subscriptions become
`on = "event"`, the block subscription becomes `on = "block"`, and the
intent-status subscription keeps its kind as videre's own extension
trigger with a `venue = "cow"` filter. Drop the zero placeholder
content pin: the key is now `[component].digest` and the runtime
verifies it against the loaded bytes, so a placeholder refuses the
load. `[venue] body_version` is unchanged.

Rename `on_chain_logs` to `on_event`, which now takes one log instead
of a batch, and adopt the keeper vocabulary rename: `WatchSet` to
`CommitmentSet`, `WatchRef` to `CommitmentRef`, `watch_key` to
`commitment_key`, and the stored prefix `watch:` to `commitment:`.
`nexum_sdk::events::ChainLogParts` becomes
`nexum_sdk::sol_events::LogParts`.

The drop-line assertion now matches on the commitment key and the verb
rather than on `composable_cow::run`'s exact wording, so the two crates
can be reworded independently.

This is a port: the submit path still relays EIP-1271 signatures only.

AI Assistance: Claude Code used for the API migration and test updates.
The runtime deleted the extension-installed component path, so a venue is
no longer a guest wasm artifact an extension installs. `videre_host::platform()`
takes no config, and the composition root registers each venue on the
`VenueRegistry` before launch.

`shepherd-engine` gains a lib target so the composition root is testable:
`ShepherdRuntime` and the new `venues` module move there, and `main.rs` is
the thin binary over it. `venues::register` reads the venue set from the
opaque `[extensions.videre]` table and builds the cow venue through
`cow_venue::register`.

Two operator capabilities are lost with `[[adapters]]`, not relocated.
Per-venue outbound-HTTP confinement is gone: `http_allow` fed the wasi:http
gate, which reaches guest components only, and a native venue owns its own
client. Operator-swappable venues are gone: `path` and `manifest` have no
successor, so changing the venue set means a new binary. Both are recorded
in the `venues` module docs and in engine.example.toml.

The eight operator configs lose `[[adapters]]` and gain
`[extensions.videre.venues.cow]`, carrying what the adapter manifest's
`[config]` table held. Each `[[modules]]` entry gains the mandatory
operator-written `id`; manifest paths become `component.toml`; the
`[limits]` scalars move to `[policy]` as `max_fuel_per_dispatch` and
`max_memory_bytes`; and every config but the reference template relaxes
`require_component_digest`, because each rebuilds its components per run.

tests/operator_configs.rs holds all eight files to the runtime's own parser
and to the venue section, so a retired key refuses in CI rather than at an
operator's boot. platform_seam.rs drops the venue world-contract test,
which had no subject left, and boots its keepers through `BootScenario`.

AI Assistance: Claude Code used for the migration and the new tests.
The runtime deleted the extension-installed component path, so a venue
can no longer be a guest wasm component. `CowAdapter` now implements
`videre_host::VenueInvoker` in-process and reaches the orderbook through
its own reqwest client instead of a scoped wasi:http import.

The orderbook protocol logic is unchanged: UID reconciliation,
already-held folding, the classification-table projection and the status
lifecycle mapping are the same code, made async over a new `Transport`
seam. `cancel` still returns `Unsupported`, because the venue holds no
keys.

The `#[videre_sdk::venue]` macros both go: the adapter export face had no
host to load it, and the client-side marker held the venue id to a
manifest `[component] name` that no longer exists. The id is now the
`VENUE_ID` literal, and `register` uses it so the registered id cannot
drift from the one the keeper client routes to.

The three `module*.toml` manifests are deleted. Their venue id, chain,
orderbook url, owner and timeout are `CowConfig`; their `body_versions`
is `BODY_VERSIONS`. Their `http_allow` grant has no in-process
equivalent, so the transport refuses redirects and the reachable host is
whatever `CowConfig::orderbook_url` names.

The `adapter` feature is renamed `venue`, the crate drops its cdylib and
`wit-bindgen`, and the wasm build of cow-venue is removed from the
justfile, CI and the Dockerfile.

AI Assistance: Claude Code used for the port, the transport seam and the tests
`shepherd-engine` called `cow_venue::register` with three arguments; it
takes two, because the venue id comes from cow-venue's own `venue_id()`
so the registered id cannot drift from the id `CowVenue::ID` routes to.

The Dockerfile still copied `module.toml`, and the e2e scripts still
grepped the `watch:` log marker, which is `commitment:` now. Neither
file was in any unit's scope.

AI Assistance: Claude Code used for the merge reconciliation.
The release branch is being retired. Its tip 1da06af is not an ancestor
of main, because the catch-up PR squash-merged, so deleting the branch
would orphan the commit this pinned. 66c8c3b carries the identical tree.

AI Assistance: Claude Code used for the re-pin.
An event trigger without `resume` re-opens at head, so every log mined
during downtime is lost. Both keepers build state only from logs, so
that loss is silent and permanent.

twap-monitor holds a commitment only because it saw the matching
`ConditionalOrderCreated`. A restart at head never learns of an order
registered while it was down, and that order is never polled or
submitted. The `ConditionalOrderRemoved` stream resumes with it:
backfilling creates alone would replay an order whose removal fell in
the same gap.

ethflow-watcher has the same shape. A placement seen once is the only
thing that puts an order under the host watch.

`max_lookback` stays unset on all three. A cap drops the oldest missed
blocks, which is the loss being fixed here.

Replay is already idempotent: `persist_commitment` overwrites in place
keeping the newest stamp, and ethflow-watcher short-circuits on
`observed:{uid}`. Both have tests. A first boot has no cursor and still
starts at head, so this backfills a restart gap only.

The two twap-monitor cursors are independent, so a backfilled removal
can land before its own create and briefly resurrect a commitment. The
next poll reverts `SingleOrderNotAuthed`, which `LegacyRevertAdapter::
classify` folds to `Invalid`, and the commitment drops.

AI Assistance: Claude Code used for the manifest change and for tracing
the resume-cursor and replay-idempotency paths.
`resume` carries a cursor across a restart, but a first boot has no
cursor and opens at head. Both keepers build their whole state from
logs, so a fresh daemon never learns of anything that happened before
it ran: a ComposableCoW conditional order can stay live for weeks, and
a TWAP registered last month is invisible and never polled.

`start_block` seeds that first boot from the contract's deployment
block, resolved from each deploy transaction:

  twap-monitor    ComposableCoW  Sepolia  5072748
  ethflow-watcher CoWSwapEthFlow Sepolia  7541028

ComposableCoW is one CREATE2 address on every chain but a different
block per chain, so mainnet is 17883049. EthFlow has had several
per-network and per-version deployments, so its block belongs to the
pinned address and must be re-derived when that address changes.

The seed applies only while no cursor is stored; after the first
committed chunk the store wins, so this is a one-time floor rather
than a rescan point. Both twap-monitor streams seed together, because
backfilling creates while removals start at head would replay every
order ever registered and retire none of them.

The pins move in lock-step: nexum-runtime fd16db7 -> 2ed882f for the
`start_block` key itself, and videre 66c8c3b -> fd8af02 for its
matching bump. A split pin resolves two copies of nexum-runtime whose
Extension seam types do not match.

Adds `module_manifests.rs`, which pins `resume` and `start_block` on
every shipped event trigger. Dropping either key still parses, still
boots and still passes every other test, so nothing else in the repo
would notice the keeper going quietly blind.

AI Assistance: Claude Code used for the pin bumps, the deployment
block resolution and the manifest pins.
@mfw78
mfw78 merged commit 94c6c69 into main Aug 31, 2026
6 checks passed
@mfw78
mfw78 deleted the chore/nexum-videre-migration branch August 31, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant