From 45e9b80b92da0315ac18ff2a1b1eafd5bb93a1ef Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 18 Jul 2026 06:09:53 +0000 Subject: [PATCH 1/4] ci: add the blocking dep-sync gate for the three-grouping workspace scripts/check-dep-sync.sh asserts the crate DAG points strictly up across nexum, videre and shepherd for every workspace member (path deps and wit/ symlinks included), and that the carve dependency artefacts agree wherever they exist: umbrella [patch] entries target the owning grouping's in-tree crates and neutralise every written git-tag pin with no orphans, pins agree on one tag per upstream repo, each Cargo.repo.toml member list mirrors the umbrella, and wit-deps manifests and locks stay key-synced with tags matching the Rust pins. Artefacts a train wave has not written yet skip visibly and enforce the moment they land. Wired as the blocking dep-sync CI job and a just check-dep-sync recipe; the git-tag to crates.io and wkg/OCI convergence path is documented in docs/design/cross-repo-deps.md. --- .github/workflows/ci.yml | 15 ++ docs/design/cross-repo-deps.md | 59 ++++++ justfile | 5 + scripts/check-dep-sync.sh | 335 +++++++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+) create mode 100644 docs/design/cross-repo-deps.md create mode 100755 scripts/check-dep-sync.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfc29390..1415435d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,3 +182,18 @@ jobs: with: tool: ripgrep - run: ./shepherd/scripts/check-cow-orderbook-only.sh + + # Blocking dep-sync gate: the three-grouping crate DAG points strictly + # up, and the carve dependency artefacts (umbrella [patch], git-tag + # pins, per-group Cargo.repo.toml, wit-deps locks) agree wherever they + # exist (scripts/check-dep-sync.sh; docs/design/cross-repo-deps.md). + dep-sync: + name: dep-sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/rust-setup + - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: ripgrep + - run: ./scripts/check-dep-sync.sh diff --git a/docs/design/cross-repo-deps.md b/docs/design/cross-repo-deps.md new file mode 100644 index 00000000..64cf3429 --- /dev/null +++ b/docs/design/cross-repo-deps.md @@ -0,0 +1,59 @@ +# Cross-repo dependencies + +How the three repos source each other after the carve, and how the +transitional umbrella keeps the monorepo building as one workspace until +then. The repos: `nullislabs/nexum-runtime` (the `nexum/` grouping, +including `nexum-world`), `nullislabs/videre-nexum-module` (`videre/`), +and `nullislabs/shepherd` staying as the app-level bundle (`shepherd/`). +Dependencies flow strictly up: shepherd on videre and nexum-runtime, +videre on nexum-runtime, nexum-runtime on neither. Plan: +`docs/design/videre-split-plan.md` (design-notes branch), section 6.2 +decisions D9 and D10. + +## Transitional umbrella (pre-carve) + +- One workspace, one hoisted dependency table, one `Cargo.lock`, atomic + folds across the groupings. +- Member manifests write cross-group Rust dependencies in their final + post-carve form: exact git-tag pins on the owning repo, one tag per + repo. +- The umbrella root `[patch]` table redirects each pinned repo URL to + the in-tree crate, so the monorepo builds the working tree, not the + tag, and stays green through the whole train. +- Each grouping carries `Cargo.repo.toml`, the workspace root it ships + with at carve time; its member list mirrors the umbrella's members + for that grouping. +- Each grouping resolves WIT crate-locally; cross-group WIT is sourced + through `wit-deps` git tags with checked-in locks + (`/wit/deps.toml` plus `deps.lock`). + +## Convergence path (decision D9) + +1. Git-tag pins, at carve time: cross-repo Rust deps are exact `tag` + pins, cross-repo WIT rides `wit-deps` git tags, locks are committed. + No semver resolution; a bump is an explicit repin downstream. +2. crates.io, once the surfaces stabilise: `nexum-runtime` and the + videre crates publish; downstreams move to caret ranges; the + `[patch]` table and the git pins disappear. +3. wkg/OCI, alongside crates.io: the WIT packages publish to + `ghcr.io/nullislabs`; `wit-deps` git sources become registry + fetches. +4. shepherd stays app-level: it pins upstream tags and never publishes. + +## Enforcement + +`scripts/check-dep-sync.sh`, blocking as the `dep-sync` CI job and +locally via `just check-dep-sync`: + +- the crate DAG points strictly up across the groupings for every + workspace member, including path deps and `wit/` symlinks; +- every umbrella `[patch]` entry targets the owning grouping's in-tree + crate and neutralises a written pin, with no orphans; +- git-tag pins agree on one tag per upstream repo everywhere they are + written; +- each `Cargo.repo.toml` member list mirrors the umbrella's; +- `wit-deps` manifests and locks exist together, stay key-synced, and + their tags agree with the Rust pins and across groups. + +A check whose artefact is not yet written skips visibly and starts +enforcing the moment the artefact lands. diff --git a/justfile b/justfile index a79f5270..5a1429cf 100644 --- a/justfile +++ b/justfile @@ -88,6 +88,11 @@ check-venue-agnostic: check-cow-orderbook-only: ./shepherd/scripts/check-cow-orderbook-only.sh +# Dep-sync gate: the three-grouping crate DAG points strictly up and +# the carve dependency artefacts agree. Blocking in CI. +check-dep-sync: + ./scripts/check-dep-sync.sh + # Check the entire workspace check: cargo check --target wasm32-wasip2 -p example diff --git a/scripts/check-dep-sync.sh b/scripts/check-dep-sync.sh new file mode 100755 index 00000000..1e530623 --- /dev/null +++ b/scripts/check-dep-sync.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash +# Dep-sync gate for the transitional three-grouping workspace. Always +# blocking: the crate DAG points strictly up (nexum <- videre <- +# shepherd) for every workspace member, and path deps and wit/ symlinks +# never cross downward. Agreement checks: the umbrella [patch] table, +# member git-tag pins, per-group Cargo.repo.toml manifests, and +# wit-deps manifests and locks must name the same crates and the same +# one tag per upstream repo. An artefact a train wave has not written +# yet skips visibly and enforces the moment it lands. See +# docs/design/cross-repo-deps.md. Run locally via `just check-dep-sync`. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." || exit 2 +root="$PWD" + +pass() { printf '\033[1;32m[dep-sync PASS]\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31m[dep-sync FAIL]\033[0m %s\n' "$*" >&2; status=1; } +skip() { printf '\033[1;33m[dep-sync SKIP]\033[0m %s\n' "$*" >&2; } + +command -v rg >/dev/null || { echo "ripgrep (rg) is required" >&2; exit 2; } +command -v cargo >/dev/null || { echo "cargo is required" >&2; exit 2; } + +status=0 + +layers=(nexum videre shepherd) +rank() { + case $1 in + nexum) echo 0 ;; + videre) echo 1 ;; + shepherd) echo 2 ;; + *) echo -1 ;; + esac +} +# Owning grouping per upstream repo; shepherd is app-level, never a +# dependency target. +slug_owner() { + case $1 in + nexum-runtime) echo nexum ;; + videre-nexum-module) echo videre ;; + *) echo "" ;; + esac +} + +crate_name() { awk -F'"' '/^name = /{print $2; exit}' "$1/Cargo.toml"; } +toml_members() { awk '/^members = \[/{f=1;next} f&&/^\]/{exit} f' "$1" | tr -d ' ",' | rg -v '^$' || true; } +path_dep_values() { rg -o 'path *= *"([^"]+)"' -r '$1' "$1" 2>/dev/null || true; } + +mapfile -t members < <(toml_members Cargo.toml) +if [[ ${#members[@]} -eq 0 ]]; then + fail "no workspace members parsed from Cargo.toml" + exit 1 +fi + +# 1. Crate DAG: every member's full build closure (normal, build, and +# its own dev edges) stays in its own grouping or above. A stale +# lock fails once here: the lock must move with the manifests. +dag_bad=0 +if ! cargo metadata --format-version 1 --locked >/dev/null; then + fail "Cargo.lock out of sync with the manifests" + dag_bad=1 +fi +[[ $dag_bad -eq 1 ]] || for m in "${members[@]}"; do + g="${m%%/*}" + r="$(rank "$g")" + name="$(crate_name "$m")" + if [[ -z $name ]]; then + fail "no crate name in $m/Cargo.toml" + dag_bad=1 + continue + fi + if ! tree="$(cargo tree -p "$name" -e normal,build,dev --all-features --prefix none --locked)"; then + fail "cargo tree failed for $name ($m)" + dag_bad=1 + continue + fi + reached="$(printf '%s\n' "$tree" | awk -v root="$root/" -v r="$r" ' + { + i = index($0, "(" root) + if (!i) next + rest = substr($0, i + 1 + length(root)) + sub(/\).*/, "", rest) + split(rest, a, "/") + gr = a[1] == "nexum" ? 0 : a[1] == "videre" ? 1 : a[1] == "shepherd" ? 2 : -1 + if (gr > r) print rest + }' | sort -u)" + if [[ -n $reached ]]; then + fail "$m reaches down-layer crates: $(tr '\n' ' ' <<<"$reached")" + dag_bad=1 + fi +done +[[ $dag_bad -eq 0 ]] && pass "crate DAG acyclic across the groupings (${#members[@]} members)" + +# 2. Path deps: resolve every member path dep; a downward edge breaks +# the DAG, and a cross-group edge into a patched grouping must +# already be a git pin (collected here, judged in section 3). +declare -A cross_path_into=() +path_bad=0 +for m in "${members[@]}"; do + g="${m%%/*}" + r="$(rank "$g")" + while IFS= read -r p; do + [[ -z $p ]] && continue + rel="$(realpath -m "$root/$m/$p")" + rel="${rel#"$root"/}" + tg="${rel%%/*}" + trank="$(rank "$tg")" + if [[ $trank -lt 0 ]]; then + fail "$m/Cargo.toml path dep escapes the workspace: $p" + path_bad=1 + elif [[ $trank -gt $r ]]; then + fail "$m/Cargo.toml path dep points down-layer: $p" + path_bad=1 + elif [[ $tg != "$g" ]]; then + cross_path_into[$tg]+="$m/Cargo.toml -> $rel " + fi + done < <(path_dep_values "$m/Cargo.toml") +done +[[ $path_bad -eq 0 ]] && pass "no downward path dep" + +# 3. Cross-group Rust pins: git-tag pins on the owning repos, one tag +# per repo, each neutralised by an umbrella [patch] entry targeting +# the owning grouping's in-tree crate, with no orphan entry and no +# leftover cross-group path dep once the patch exists. +# Live manifests are read by the umbrella build and need [patch] +# neutralisation; Cargo.repo.toml is inert until the carve and only +# joins the tag-agreement check. +manifest_files=(Cargo.toml) +for m in "${members[@]}"; do manifest_files+=("$m/Cargo.toml"); done +for g in "${layers[@]}"; do + [[ -f $g/Cargo.repo.toml ]] && manifest_files+=("$g/Cargo.repo.toml") +done + +declare -A tag_for=() +for slug in nexum-runtime videre-nexum-module; do + url="https://github.com/nullislabs/$slug" + owner="$(slug_owner "$slug")" + pins="$(rg -Hn --no-heading "git *= *\"${url}(\\.git)?\"" "${manifest_files[@]}" || true)" + patch_body="$(awk -v hdr="[patch.\"$url\"]" ' + index($0, hdr) == 1 {f=1; next} + /^\[/{f=0} + f && NF && $0 !~ /^[[:space:]]*#/' Cargo.toml)" + + if [[ -z $pins && -z $patch_body ]]; then + skip "$slug pins and umbrella patch not written yet" + continue + fi + slug_ok=1 + + pin_crates="" + tags="" + live_pins=0 + while IFS= read -r hit; do + [[ -z $hit ]] && continue + file="${hit%%:*}" + loc="$file:$(cut -d: -f2 <<<"$hit")" + line="${hit#*:}" + line="${line#*:}" + if [[ $line =~ tag\ *=\ *\"([^\"]+)\" ]]; then + tags+="${BASH_REMATCH[1]}"$'\n' + else + fail "$loc: $slug git pin without an inline tag" + slug_ok=0 + fi + # Inert Cargo.repo.toml pins join the tag agreement only. + [[ $file == */Cargo.repo.toml ]] && continue + live_pins=1 + pin_crates+="$(sed -E 's/^[[:space:]]*([A-Za-z0-9_-]+)[[:space:]]*=.*/\1/' <<<"$line")"$'\n' + done <<<"$pins" + pin_crates="$(sort -u <<<"$pin_crates" | rg -v '^$' || true)" + uniq_tags="$(sort -u <<<"$tags" | rg -v '^$' || true)" + if [[ -n $uniq_tags && $(wc -l <<<"$uniq_tags") -gt 1 ]]; then + fail "$slug pinned at more than one tag: $(tr '\n' ' ' <<<"$uniq_tags")" + slug_ok=0 + elif [[ -n $uniq_tags ]]; then + tag_for[$slug]="$uniq_tags" + fi + + if [[ $live_pins -eq 1 && -z $patch_body ]]; then + fail "umbrella [patch] section missing for $url" + continue + fi + if [[ $live_pins -eq 0 && -z $patch_body ]]; then + [[ $slug_ok -eq 1 ]] && pass "$slug pinned at ${tag_for[$slug]:-?} (inert manifests only)" + continue + fi + + patch_keys="" + while IFS= read -r entry; do + [[ -z $entry ]] && continue + key="$(sed -E 's/^[[:space:]]*([A-Za-z0-9_-]+)[[:space:]]*=.*/\1/' <<<"$entry")" + p="$(rg -o 'path *= *"([^"]+)"' -r '$1' <<<"$entry" || true)" + patch_keys+="$key"$'\n' + if [[ -z $p ]]; then + fail "umbrella patch entry $key for $slug carries no path" + slug_ok=0 + continue + fi + if [[ ! -f $p/Cargo.toml ]]; then + fail "umbrella patch $key -> $p: no crate there" + slug_ok=0 + continue + fi + if [[ "${p%%/*}" != "$owner" ]]; then + fail "umbrella patch $key -> $p leaves the $owner grouping" + slug_ok=0 + fi + if [[ "$(crate_name "$p")" != "$key" ]]; then + fail "umbrella patch $key -> $p names crate $(crate_name "$p")" + slug_ok=0 + fi + done <<<"$patch_body" + patch_keys="$(sort -u <<<"$patch_keys" | rg -v '^$' || true)" + + unpatched="$(comm -23 <(printf '%s\n' "${pin_crates:-}") <(printf '%s\n' "${patch_keys:-}") | rg -v '^$' || true)" + if [[ -n $unpatched ]]; then + fail "$slug pins not neutralised by the umbrella patch: $(tr '\n' ' ' <<<"$unpatched")" + slug_ok=0 + fi + orphans="$(comm -13 <(printf '%s\n' "${pin_crates:-}") <(printf '%s\n' "${patch_keys:-}") | rg -v '^$' || true)" + if [[ -n $orphans ]]; then + fail "umbrella patch entries with no $slug pin: $(tr '\n' ' ' <<<"$orphans")" + slug_ok=0 + fi + + if [[ -n ${cross_path_into[$owner]:-} ]]; then + fail "cross-group path deps into $owner/ must be $slug git pins: ${cross_path_into[$owner]}" + slug_ok=0 + fi + [[ $slug_ok -eq 1 ]] && pass "$slug pinned at ${tag_for[$slug]:-?} and neutralised by the umbrella patch" +done + +# 4. Per-group Cargo.repo.toml: the carve-time workspace root must list +# exactly the umbrella's members for its grouping, and its path deps +# must stay inside the grouping. +for g in "${layers[@]}"; do + f="$g/Cargo.repo.toml" + if [[ ! -f $f ]]; then + skip "$f not written yet" + continue + fi + repo_members="$(toml_members "$f" | sort)" + expect="$(printf '%s\n' "${members[@]}" | rg "^$g/" | sed "s|^$g/||" | sort)" + if [[ "$repo_members" != "$expect" ]]; then + fail "$f member list disagrees with the umbrella workspace:" + diff <(printf '%s\n' "$expect") <(printf '%s\n' "$repo_members") >&2 || true + else + pass "$f members mirror the umbrella" + fi + while IFS= read -r p; do + [[ -z $p ]] && continue + rel="$(realpath -m "$root/$g/$p")" + [[ $rel == "$root/$g/"* ]] || fail "$f path dep leaves the grouping: $p" + done < <(path_dep_values "$f") +done + +# 5. WIT: cross-group wit/ symlinks never point down-layer; wit-deps +# manifests and locks exist together, stay key-synced, source only +# up-layer repos, and their tags agree with the Rust pins and across +# groups: one tag per upstream repo everywhere. +wit_bad=0 +declare -A wit_tag_seen=() +declare -A wit_tag_where=() +for g in "${layers[@]}"; do + r="$(rank "$g")" + [[ -d $g/wit ]] || continue + while IFS= read -r link; do + rel="$(realpath -m "$link")" + rel="${rel#"$root"/}" + trank="$(rank "${rel%%/*}")" + if [[ $trank -lt 0 || $trank -gt $r ]]; then + fail "$link symlink points down-layer or outside: $rel" + wit_bad=1 + fi + done < <(find "$g/wit" -type l) + + toml="$g/wit/deps.toml" + lock="$g/wit/deps.lock" + if [[ ! -f $toml && ! -f $lock ]]; then + skip "$g wit-deps manifest not written yet" + continue + fi + if [[ ! -f $toml || ! -f $lock ]]; then + fail "$g wit-deps manifest and lock must exist together" + wit_bad=1 + continue + fi + if [[ $g == nexum ]]; then + if rg -q '^\s*[^#[:space:]]' "$toml" "$lock"; then + fail "nexum wit-deps must stay empty; nexum:host is a leaf" + wit_bad=1 + fi + continue + fi + while IFS= read -r key; do + [[ -z $key ]] && continue + if ! rg -q "^(\\[$key\\]|$key *=)" "$lock"; then + fail "$toml key $key missing from $lock" + wit_bad=1 + fi + done < <({ rg -o '^([A-Za-z0-9_-]+) *=' -r '$1' "$toml" || true; rg -o '^\[([A-Za-z0-9_-]+)\]' -r '$1' "$toml" || true; } | sort -u) + for slug in nexum-runtime videre-nexum-module shepherd; do + owner="$(slug_owner "$slug")" + owner_rank="$(rank "${owner:-shepherd}")" + hits="$(rg --no-filename "nullislabs/$slug" "$toml" "$lock" || true)" + [[ -z $hits ]] && continue + if [[ $owner_rank -ge $r ]]; then + fail "$g wit-deps sources nullislabs/$slug, not an up-layer repo" + wit_bad=1 + continue + fi + wit_tags="$({ + rg -o 'refs/tags/([^/"]+)' -r '$1' <<<"$hits" | sed -E 's/\.(tar\.gz|tgz|zip)$//' + rg -o 'tag *= *"([^"]+)"' -r '$1' <<<"$hits" + } | sort -u | rg -v '^$' || true)" + [[ -z $wit_tags ]] && continue + if [[ $(wc -l <<<"$wit_tags") -gt 1 ]]; then + fail "$g wit-deps pins nullislabs/$slug at more than one tag: $(tr '\n' ' ' <<<"$wit_tags")" + wit_bad=1 + elif [[ -n ${tag_for[$slug]:-} && $wit_tags != "${tag_for[$slug]}" ]]; then + fail "$g wit-deps tag $wit_tags disagrees with the Rust pin ${tag_for[$slug]} for nullislabs/$slug" + wit_bad=1 + elif [[ -n ${wit_tag_seen[$slug]:-} && $wit_tags != "${wit_tag_seen[$slug]}" ]]; then + fail "$g wit-deps tag $wit_tags disagrees with the ${wit_tag_where[$slug]} wit-deps tag ${wit_tag_seen[$slug]} for nullislabs/$slug" + wit_bad=1 + else + wit_tag_seen[$slug]="$wit_tags" + wit_tag_where[$slug]="$g" + fi + done +done +[[ $wit_bad -eq 0 ]] && pass "wit artefacts agree where written" + +exit "$status" From 959342905d3617826cc354eb2437c14efb34d5f7 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 18 Jul 2026 04:55:25 +0000 Subject: [PATCH 2/4] packaging: pin nexum:host WIT resolution group-local with an empty wit-deps manifest The runtime bindgen and every L1 fixture module already resolve wit/nexum-host inside the nexum group; check in an empty wit/deps.toml plus its lock to pin the leaf, and teach the zero-leak gate to fail if either ever declares a dependency. --- nexum/crates/nexum-runtime/src/bindings.rs | 3 ++- nexum/scripts/check-venue-agnostic.sh | 18 +++++++++++++++++- nexum/wit/deps.lock | 0 nexum/wit/deps.toml | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 nexum/wit/deps.lock create mode 100644 nexum/wit/deps.toml diff --git a/nexum/crates/nexum-runtime/src/bindings.rs b/nexum/crates/nexum-runtime/src/bindings.rs index 46fb2424..b3debecb 100644 --- a/nexum/crates/nexum-runtime/src/bindings.rs +++ b/nexum/crates/nexum-runtime/src/bindings.rs @@ -11,7 +11,8 @@ //! //! `nexum:host` is a leaf package: the host `event` variant carries a //! status transition as opaque bytes, so the core world resolves against -//! `wit/nexum-host` alone. An extension's bindgen remaps onto the shared +//! `wit/nexum-host` alone; the group `wit/deps.toml` and its lock stay +//! empty. An extension's bindgen remaps onto the shared //! interfaces here with `with`, so the `Host` impls and the `fault` type //! its components see are the very ones the core host constructs. //! `PartialEq` is derived so extension services can compare event payloads. diff --git a/nexum/scripts/check-venue-agnostic.sh b/nexum/scripts/check-venue-agnostic.sh index 407006c9..d7658e44 100755 --- a/nexum/scripts/check-venue-agnostic.sh +++ b/nexum/scripts/check-venue-agnostic.sh @@ -5,7 +5,8 @@ # charter symbol # (nexum:intent|value-flow|VenueAdapter|synthesize_venue|nexum:adapter|PoolRouter) # and no privileged router field; and nexum:host names no foreign WIT -# package and resolves as a leaf. The opaque-status envelope +# package, resolves as a leaf, and its wit-deps manifest and lock stay +# empty. The opaque-status envelope # (intent-status-update, its venue id string) is ratified host surface, # not a leak. Blocking in CI; run locally via `just check-venue-agnostic`. @@ -84,4 +85,19 @@ else printf '\033[1;33m[l1 WARN]\033[0m wasm-tools not found; WIT resolve skipped\n' >&2 fi +# 5. wit-deps manifest: crate-local resolution with an empty, locked +# dependency set; a declared dependency would unmake the leaf. +for f in wit/deps.toml wit/deps.lock; do + if [[ ! -f $f ]]; then + fail "$f missing" + continue + fi + rg -n --no-heading -e '^\s*[^#[:space:]]' "$f" + case $? in + 0) fail "$f declares a WIT dependency; nexum:host is a leaf" ;; + 1) pass "$f empty" ;; + *) fail "manifest scan errored for $f" ;; + esac +done + exit "$status" diff --git a/nexum/wit/deps.lock b/nexum/wit/deps.lock new file mode 100644 index 00000000..e69de29b diff --git a/nexum/wit/deps.toml b/nexum/wit/deps.toml new file mode 100644 index 00000000..e313c703 --- /dev/null +++ b/nexum/wit/deps.toml @@ -0,0 +1 @@ +# nexum:host is a leaf package; this manifest stays empty. From 55f8aec5cbe426af75b0619749d6d516777cd93c Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 18 Jul 2026 06:27:25 +0000 Subject: [PATCH 3/4] packaging: vendor nexum:host WIT into the videre group via wit-deps Replace the cross-group nexum-host symlink with a wit-deps vendor: wit/deps.toml pins the package to an exact nexum-runtime git tag, wit/deps.lock records the content digests, and wit/deps/nexum-host is checked in so the group resolves WIT crate-locally and offline. The videre-host and videre-sdk bindgens now read wit/deps/nexum-host. --- videre/crates/videre-host/src/bindings.rs | 6 +- videre/crates/videre-sdk/src/bindings.rs | 2 +- videre/wit/deps.lock | 5 + videre/wit/deps.toml | 8 ++ videre/wit/deps/nexum-host/chain.wit | 60 +++++++++++ videre/wit/deps/nexum-host/event-module.wit | 24 +++++ videre/wit/deps/nexum-host/identity.wit | 24 +++++ videre/wit/deps/nexum-host/local-store.wit | 29 ++++++ videre/wit/deps/nexum-host/logging.wit | 15 +++ videre/wit/deps/nexum-host/messaging.wit | 19 ++++ videre/wit/deps/nexum-host/query-module.wit | 25 +++++ videre/wit/deps/nexum-host/remote-store.wit | 33 ++++++ videre/wit/deps/nexum-host/types.wit | 106 ++++++++++++++++++++ videre/wit/nexum-host | 1 - 14 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 videre/wit/deps.lock create mode 100644 videre/wit/deps.toml create mode 100644 videre/wit/deps/nexum-host/chain.wit create mode 100644 videre/wit/deps/nexum-host/event-module.wit create mode 100644 videre/wit/deps/nexum-host/identity.wit create mode 100644 videre/wit/deps/nexum-host/local-store.wit create mode 100644 videre/wit/deps/nexum-host/logging.wit create mode 100644 videre/wit/deps/nexum-host/messaging.wit create mode 100644 videre/wit/deps/nexum-host/query-module.wit create mode 100644 videre/wit/deps/nexum-host/remote-store.wit create mode 100644 videre/wit/deps/nexum-host/types.wit delete mode 120000 videre/wit/nexum-host diff --git a/videre/crates/videre-host/src/bindings.rs b/videre/crates/videre-host/src/bindings.rs index 06e2b887..28f35449 100644 --- a/videre/crates/videre-host/src/bindings.rs +++ b/videre/crates/videre-host/src/bindings.rs @@ -19,7 +19,7 @@ mod venue_adapter { path: [ "../../wit/videre-value-flow", "../../wit/videre-types", - "../../wit/nexum-host", + "../../wit/deps/nexum-host", "../../wit/videre-venue", ], world: "videre:venue/venue-adapter", @@ -54,7 +54,7 @@ mod client_host { path: [ "../../wit/videre-value-flow", "../../wit/videre-types", - "../../wit/nexum-host", + "../../wit/deps/nexum-host", "../../wit/videre-venue", ], imports: { default: async }, @@ -171,7 +171,7 @@ mod client_smoke { path: [ "../../wit/videre-value-flow", "../../wit/videre-types", - "../../wit/nexum-host", + "../../wit/deps/nexum-host", "../../wit/videre-venue", ], }); diff --git a/videre/crates/videre-sdk/src/bindings.rs b/videre/crates/videre-sdk/src/bindings.rs index 9f57dde8..77a561a4 100644 --- a/videre/crates/videre-sdk/src/bindings.rs +++ b/videre/crates/videre-sdk/src/bindings.rs @@ -34,7 +34,7 @@ world sdk-imports { path: [ "../../wit/videre-value-flow", "../../wit/videre-types", - "../../wit/nexum-host", + "../../wit/deps/nexum-host", "../../wit/videre-venue", ], world: "videre:sdk-shims/sdk-imports", diff --git a/videre/wit/deps.lock b/videre/wit/deps.lock new file mode 100644 index 00000000..76e2f363 --- /dev/null +++ b/videre/wit/deps.lock @@ -0,0 +1,5 @@ +[nexum-host] +url = "https://github.com/nullislabs/nexum-runtime/archive/refs/tags/v0.1.0.tar.gz" +prefix = "nexum-runtime-0.1.0/wit/nexum-host" +sha256 = "52211dea6cf662c1b00b4fa705e5f9e05a44fb3d114b7cc9c0f4cdb77b2fc716" +sha512 = "9ed9bff008c7bb0620250c26b59fc0fdec5ac7326d1d5bc4e81dfdaad18c357444cb0786b8a53e2ff4090f5cfcab956a56efabe0718985c7871b82e30ce0df8d" diff --git a/videre/wit/deps.toml b/videre/wit/deps.toml new file mode 100644 index 00000000..053a4751 --- /dev/null +++ b/videre/wit/deps.toml @@ -0,0 +1,8 @@ +# Cross-repo WIT pins, vendored under wit/deps and verified against +# deps.lock by `wit-deps`. Pins are exact git tags of the owning repo; +# they converge to wkg/OCI (ghcr.io/nullislabs) with per-package semver +# once the packages are published. + +[nexum-host] +url = "https://github.com/nullislabs/nexum-runtime/archive/refs/tags/v0.1.0.tar.gz" +prefix = "nexum-runtime-0.1.0/wit/nexum-host" diff --git a/videre/wit/deps/nexum-host/chain.wit b/videre/wit/deps/nexum-host/chain.wit new file mode 100644 index 00000000..1d812dc6 --- /dev/null +++ b/videre/wit/deps/nexum-host/chain.wit @@ -0,0 +1,60 @@ +package nexum:host@0.1.0; + +interface chain { + use types.{chain-id, fault}; + + /// A JSON-RPC error response from the upstream node. `code` is the + /// node-reported numeric (typically `-32000` for an `eth_call` + /// revert). `data` is the decoded `error.data` payload: the host + /// hex-decodes the upstream JSON string once, so a guest receives + /// the raw abi-encoded revert bytes without any string handling. + record rpc-error { + code: s32, + message: string, + data: option>, + } + + /// Failure of a chain call: either a shared host `fault` (transport + /// down, timed out, denied, ...) or a structured JSON-RPC error + /// carrying the node code and any decoded revert payload. + variant chain-error { + fault(fault), + rpc(rpc-error), + } + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(chain-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). Each + /// host enforces its own permitted method surface; the reference + /// server host forwards only a read-only set and refuses signing + /// or mutating methods with a `denied` fault before they reach the + /// provider. `params` and the success value are JSON-encoded + /// strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. Hosts that cannot + /// batch natively MUST fall back to sequential `request` calls. The + /// returned list is the same length as `requests` and in the same order. + request-batch: func(chain-id: chain-id, requests: list) + -> result, chain-error>; +} diff --git a/videre/wit/deps/nexum-host/event-module.wit b/videre/wit/deps/nexum-host/event-module.wit new file mode 100644 index 00000000..277134a9 --- /dev/null +++ b/videre/wit/deps/nexum-host/event-module.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, fault}; + + // Six core primitives (always provided by a conforming host). + import chain; + import identity; + import local-store; + import remote-store; + import messaging; + import logging; + + // Time, randomness, and outbound HTTP are WASI concerns, not + // nexum:host interfaces: wasi:clocks and wasi:random are linked + // into every module store, and hosts link + // wasi:http/outgoing-handler, gated per-module by the + // `[capabilities.http].allow` allowlist in module.toml. + + export init: func(config: config) -> result<_, fault>; + export on-event: func(event: event) -> result<_, fault>; +} diff --git a/videre/wit/deps/nexum-host/identity.wit b/videre/wit/deps/nexum-host/identity.wit new file mode 100644 index 00000000..2e82c9fe --- /dev/null +++ b/videre/wit/deps/nexum-host/identity.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `fault.denied`. +interface identity { + use types.{fault}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, fault>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, fault>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/local-store.wit b/videre/wit/deps/nexum-host/local-store.wit new file mode 100644 index 00000000..8521b37a --- /dev/null +++ b/videre/wit/deps/nexum-host/local-store.wit @@ -0,0 +1,29 @@ +package nexum:host@0.1.0; + +interface local-store { + use types.{fault}; + + /// Get a value by key. Returns none if the key does not exist. + get: func(key: string) -> result>, fault>; + + /// Set a key-value pair. Overwrites any existing value. + /// The host may enforce a size quota; if exceeded, returns err. + set: func(key: string, value: list) -> result<_, fault>; + + /// Delete a key. No-op if the key does not exist. + delete: func(key: string) -> result<_, fault>; + + /// List all keys matching a prefix. Empty prefix returns all keys. + list-keys: func(prefix: string) -> result, fault>; + + /// Whether the key exists, without transferring the value. + contains: func(key: string) -> result; + + /// Value byte length, none if the key is absent, without + /// transferring the value. On some backends this may be a scan. + len: func(key: string) -> result, fault>; + + /// Number of keys matching a prefix, without materialising the key + /// list. On some backends this may be a scan. + count: func(prefix: string) -> result; +} diff --git a/videre/wit/deps/nexum-host/logging.wit b/videre/wit/deps/nexum-host/logging.wit new file mode 100644 index 00000000..8cd98140 --- /dev/null +++ b/videre/wit/deps/nexum-host/logging.wit @@ -0,0 +1,15 @@ +package nexum:host@0.1.0; + +interface logging { + enum level { + trace, + debug, + info, + warn, + error, + } + + /// Emit a structured log message. + /// The host decides how to handle it (stdout, file, discard). + log: func(level: level, message: string); +} diff --git a/videre/wit/deps/nexum-host/messaging.wit b/videre/wit/deps/nexum-host/messaging.wit new file mode 100644 index 00000000..30777ca7 --- /dev/null +++ b/videre/wit/deps/nexum-host/messaging.wit @@ -0,0 +1,19 @@ +package nexum:host@0.1.0; + +interface messaging { + use types.{fault, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, fault>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/query-module.wit b/videre/wit/deps/nexum-host/query-module.wit new file mode 100644 index 00000000..ffb29b87 --- /dev/null +++ b/videre/wit/deps/nexum-host/query-module.wit @@ -0,0 +1,25 @@ +package nexum:host@0.1.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, fault}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, fault>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/remote-store.wit b/videre/wit/deps/nexum-host/remote-store.wit new file mode 100644 index 00000000..39b36bae --- /dev/null +++ b/videre/wit/deps/nexum-host/remote-store.wit @@ -0,0 +1,33 @@ +package nexum:host@0.1.0; + +interface remote-store { + use types.{fault}; + + /// Upload raw data to the decentralised store. + /// Returns the 32-byte content reference (Swarm address). + upload: func(data: list) -> result, fault>; + + /// Download raw data by 32-byte content reference. + download: func(reference: list) -> result, fault>; + + /// Read the latest value from a mutable feed. + /// + /// Feeds are mutable pointers: (owner, topic) -> latest chunk. + /// `owner`: 20-byte Ethereum address of the feed owner. + /// `topic`: 32-byte topic hash. + read-feed: func( + owner: list, + topic: list, + ) -> result>, fault>; + + /// Update a mutable feed with new data. + /// + /// The host signs the feed update with its configured identity. + /// `topic`: 32-byte topic hash. + /// `data`: the payload to publish. + /// Returns the 32-byte reference of the new chunk. + write-feed: func( + topic: list, + data: list, + ) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/types.wit b/videre/wit/deps/nexum-host/types.wit new file mode 100644 index 00000000..8e834263 --- /dev/null +++ b/videre/wit/deps/nexum-host/types.wit @@ -0,0 +1,106 @@ +package nexum:host@0.1.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC. +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + /// One decoded log, mirroring the RPC `eth_getLogs` shape field for + /// field so the guest can rebuild the native alloy log losslessly. + /// Fixed-width byte fields are carried raw: `address` is 20 bytes, + /// each topic and hash is 32. The block-scoped fields are absent on a + /// pending log (mined logs carry them all). + record chain-log { + address: list, + topics: list>, + data: list, + block-hash: option>, + block-number: option, + block-timestamp: option, + transaction-hash: option>, + transaction-index: option, + log-index: option, + removed: bool, + } + + /// A batch of logs delivered from one subscription. The alloy log type + /// carries no chain id, so it sits here once: every log in a delivery + /// shares the chain of the subscription that produced it. + record chain-logs { + chain-id: chain-id, + logs: list, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + /// A host-observed status transition for a previously submitted + /// intent. Transport-blind: the subscriber sees only where the + /// intent is in its life, never how the host learnt it. + record intent-status-update { + /// Venue id the receipt was issued by. + venue: string, + /// The venue-scoped intent identifier, opaque to the host. + receipt: list, + /// Opaque versioned status body: a leading u8 version tag, + /// then that version's borsh payload (v1: lifecycle status, + /// optional settlement proof, optional failure reason). An + /// unknown tag is a decode error and the body is never empty. + /// The host never inspects it. + status: list, + } + + variant event { + block(block), + chain-logs(chain-logs), + tick(tick), + message(message), + intent-status(intent-status-update), + } + + /// Opaque config from module.toml [config] section. + type config = list>; + + /// The cross-domain failure vocabulary richer interfaces embed as a + /// case. Each payload-bearing case carries a human-readable detail; + /// `rate-limited` carries structured backoff guidance instead. + variant fault { + unsupported(string), + unavailable(string), + denied(string), + rate-limited(rate-limit), + timeout, + invalid-input(string), + internal(string), + } + + /// Backoff guidance for a `fault.rate-limited`. `retry-after-ms` is + /// the host's suggested wait before retrying, when known. + record rate-limit { + retry-after-ms: option, + } +} diff --git a/videre/wit/nexum-host b/videre/wit/nexum-host deleted file mode 120000 index 9b9eefce..00000000 --- a/videre/wit/nexum-host +++ /dev/null @@ -1 +0,0 @@ -../../nexum/wit/nexum-host \ No newline at end of file From 1978721b1c793ecd4e738396b71f0554928c00e5 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 18 Jul 2026 06:01:00 +0000 Subject: [PATCH 4/4] packaging: vendor cross-group WIT into shepherd/wit/deps Replace the symlinked nexum-host and videre packages with vendored copies under wit/deps, pinned by wit/deps.toml release-tag sources, and point the layering gate at the vendored tree. --- .../crates/cow-venue/tests/wit_layering.rs | 24 ++-- shepherd/wit/deps.lock | 23 ++++ shepherd/wit/deps.toml | 21 ++++ shepherd/wit/deps/nexum-host/chain.wit | 60 ++++++++++ shepherd/wit/deps/nexum-host/event-module.wit | 24 ++++ shepherd/wit/deps/nexum-host/identity.wit | 24 ++++ shepherd/wit/deps/nexum-host/local-store.wit | 29 +++++ shepherd/wit/deps/nexum-host/logging.wit | 15 +++ shepherd/wit/deps/nexum-host/messaging.wit | 19 ++++ shepherd/wit/deps/nexum-host/query-module.wit | 25 +++++ shepherd/wit/deps/nexum-host/remote-store.wit | 33 ++++++ shepherd/wit/deps/nexum-host/types.wit | 106 ++++++++++++++++++ shepherd/wit/deps/videre-types/types.wit | 83 ++++++++++++++ shepherd/wit/deps/videre-value-flow/types.wit | 32 ++++++ shepherd/wit/deps/videre-venue/venue.wit | 49 ++++++++ shepherd/wit/nexum-host | 1 - shepherd/wit/videre-types | 1 - shepherd/wit/videre-value-flow | 1 - shepherd/wit/videre-venue | 1 - 19 files changed, 557 insertions(+), 14 deletions(-) create mode 100644 shepherd/wit/deps.lock create mode 100644 shepherd/wit/deps.toml create mode 100644 shepherd/wit/deps/nexum-host/chain.wit create mode 100644 shepherd/wit/deps/nexum-host/event-module.wit create mode 100644 shepherd/wit/deps/nexum-host/identity.wit create mode 100644 shepherd/wit/deps/nexum-host/local-store.wit create mode 100644 shepherd/wit/deps/nexum-host/logging.wit create mode 100644 shepherd/wit/deps/nexum-host/messaging.wit create mode 100644 shepherd/wit/deps/nexum-host/query-module.wit create mode 100644 shepherd/wit/deps/nexum-host/remote-store.wit create mode 100644 shepherd/wit/deps/nexum-host/types.wit create mode 100644 shepherd/wit/deps/videre-types/types.wit create mode 100644 shepherd/wit/deps/videre-value-flow/types.wit create mode 100644 shepherd/wit/deps/videre-venue/venue.wit delete mode 120000 shepherd/wit/nexum-host delete mode 120000 shepherd/wit/videre-types delete mode 120000 shepherd/wit/videre-value-flow delete mode 120000 shepherd/wit/videre-venue diff --git a/shepherd/crates/cow-venue/tests/wit_layering.rs b/shepherd/crates/cow-venue/tests/wit_layering.rs index 4ccf59ed..debeb70e 100644 --- a/shepherd/crates/cow-venue/tests/wit_layering.rs +++ b/shepherd/crates/cow-venue/tests/wit_layering.rs @@ -1,17 +1,15 @@ -//! Layering gate: no generic WIT package references `shepherd:cow`. -//! The bundle-layer package carries only the event ABIs; the generic -//! host and videre packages must never name it. +//! Layering gate: no vendored generic WIT package references +//! `shepherd:cow`. The bundle-layer package carries only the event +//! ABIs; the vendored host and videre packages must never name it. use std::path::Path; #[test] -fn generic_wit_packages_never_reference_shepherd_cow() { - let wit_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../wit"); - for pkg in std::fs::read_dir(&wit_root).expect("wit dir") { - let pkg = pkg.expect("wit dir entry").path(); - if pkg.file_name().is_some_and(|n| n == "shepherd-cow") { - continue; - } +fn vendored_wit_packages_never_reference_shepherd_cow() { + let deps_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../wit/deps"); + let mut seen = 0usize; + for pkg in std::fs::read_dir(&deps_root).expect("wit/deps dir") { + let pkg = pkg.expect("wit/deps entry").path(); for file in std::fs::read_dir(&pkg).expect("wit package dir") { let path = file.expect("wit package entry").path(); let text = std::fs::read_to_string(&path).expect("read wit file"); @@ -20,6 +18,12 @@ fn generic_wit_packages_never_reference_shepherd_cow() { "{} references shepherd:cow", path.display(), ); + seen += 1; } } + assert!( + seen > 0, + "no vendored wit files under {}", + deps_root.display() + ); } diff --git a/shepherd/wit/deps.lock b/shepherd/wit/deps.lock new file mode 100644 index 00000000..1448fa5d --- /dev/null +++ b/shepherd/wit/deps.lock @@ -0,0 +1,23 @@ +[nexum-host] +url = "https://github.com/nullislabs/nexum-runtime/archive/refs/tags/v0.1.0.tar.gz" +prefix = "nexum-runtime-0.1.0/wit/nexum-host" +sha256 = "52211dea6cf662c1b00b4fa705e5f9e05a44fb3d114b7cc9c0f4cdb77b2fc716" +sha512 = "9ed9bff008c7bb0620250c26b59fc0fdec5ac7326d1d5bc4e81dfdaad18c357444cb0786b8a53e2ff4090f5cfcab956a56efabe0718985c7871b82e30ce0df8d" + +[videre-types] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-types" +sha256 = "e032daedaf625e2286a6656ae259700135df3c58446e9fd728b602f856c6baa6" +sha512 = "77e0297bb0e82cc36dc5aba705edce0edd200b2c5765044f84ad483718487fa3f94c74337e5d66528ed41d72b13b161c924d35ec7514d154ebcce9ef0438fa1a" + +[videre-value-flow] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-value-flow" +sha256 = "e032daedaf625e2286a6656ae259700135df3c58446e9fd728b602f856c6baa6" +sha512 = "77e0297bb0e82cc36dc5aba705edce0edd200b2c5765044f84ad483718487fa3f94c74337e5d66528ed41d72b13b161c924d35ec7514d154ebcce9ef0438fa1a" + +[videre-venue] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-venue" +sha256 = "e032daedaf625e2286a6656ae259700135df3c58446e9fd728b602f856c6baa6" +sha512 = "77e0297bb0e82cc36dc5aba705edce0edd200b2c5765044f84ad483718487fa3f94c74337e5d66528ed41d72b13b161c924d35ec7514d154ebcce9ef0438fa1a" diff --git a/shepherd/wit/deps.toml b/shepherd/wit/deps.toml new file mode 100644 index 00000000..50920682 --- /dev/null +++ b/shepherd/wit/deps.toml @@ -0,0 +1,21 @@ +# Cross-repo WIT pins, vendored under wit/deps and verified against +# deps.lock by `wit-deps`. Pins are exact git tags of the owning repo, +# one tag per upstream, agreeing with the Rust pins; they converge to +# wkg/OCI (ghcr.io/nullislabs) with per-package semver once the +# packages are published. + +[nexum-host] +url = "https://github.com/nullislabs/nexum-runtime/archive/refs/tags/v0.1.0.tar.gz" +prefix = "nexum-runtime-0.1.0/wit/nexum-host" + +[videre-types] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-types" + +[videre-value-flow] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-value-flow" + +[videre-venue] +url = "https://github.com/nullislabs/videre-nexum-module/archive/refs/tags/v0.1.0.tar.gz" +prefix = "videre-nexum-module-0.1.0/wit/videre-venue" diff --git a/shepherd/wit/deps/nexum-host/chain.wit b/shepherd/wit/deps/nexum-host/chain.wit new file mode 100644 index 00000000..1d812dc6 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/chain.wit @@ -0,0 +1,60 @@ +package nexum:host@0.1.0; + +interface chain { + use types.{chain-id, fault}; + + /// A JSON-RPC error response from the upstream node. `code` is the + /// node-reported numeric (typically `-32000` for an `eth_call` + /// revert). `data` is the decoded `error.data` payload: the host + /// hex-decodes the upstream JSON string once, so a guest receives + /// the raw abi-encoded revert bytes without any string handling. + record rpc-error { + code: s32, + message: string, + data: option>, + } + + /// Failure of a chain call: either a shared host `fault` (transport + /// down, timed out, denied, ...) or a structured JSON-RPC error + /// carrying the node code and any decoded revert payload. + variant chain-error { + fault(fault), + rpc(rpc-error), + } + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(chain-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). Each + /// host enforces its own permitted method surface; the reference + /// server host forwards only a read-only set and refuses signing + /// or mutating methods with a `denied` fault before they reach the + /// provider. `params` and the success value are JSON-encoded + /// strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. Hosts that cannot + /// batch natively MUST fall back to sequential `request` calls. The + /// returned list is the same length as `requests` and in the same order. + request-batch: func(chain-id: chain-id, requests: list) + -> result, chain-error>; +} diff --git a/shepherd/wit/deps/nexum-host/event-module.wit b/shepherd/wit/deps/nexum-host/event-module.wit new file mode 100644 index 00000000..277134a9 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/event-module.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, fault}; + + // Six core primitives (always provided by a conforming host). + import chain; + import identity; + import local-store; + import remote-store; + import messaging; + import logging; + + // Time, randomness, and outbound HTTP are WASI concerns, not + // nexum:host interfaces: wasi:clocks and wasi:random are linked + // into every module store, and hosts link + // wasi:http/outgoing-handler, gated per-module by the + // `[capabilities.http].allow` allowlist in module.toml. + + export init: func(config: config) -> result<_, fault>; + export on-event: func(event: event) -> result<_, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/identity.wit b/shepherd/wit/deps/nexum-host/identity.wit new file mode 100644 index 00000000..2e82c9fe --- /dev/null +++ b/shepherd/wit/deps/nexum-host/identity.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `fault.denied`. +interface identity { + use types.{fault}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, fault>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, fault>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/local-store.wit b/shepherd/wit/deps/nexum-host/local-store.wit new file mode 100644 index 00000000..8521b37a --- /dev/null +++ b/shepherd/wit/deps/nexum-host/local-store.wit @@ -0,0 +1,29 @@ +package nexum:host@0.1.0; + +interface local-store { + use types.{fault}; + + /// Get a value by key. Returns none if the key does not exist. + get: func(key: string) -> result>, fault>; + + /// Set a key-value pair. Overwrites any existing value. + /// The host may enforce a size quota; if exceeded, returns err. + set: func(key: string, value: list) -> result<_, fault>; + + /// Delete a key. No-op if the key does not exist. + delete: func(key: string) -> result<_, fault>; + + /// List all keys matching a prefix. Empty prefix returns all keys. + list-keys: func(prefix: string) -> result, fault>; + + /// Whether the key exists, without transferring the value. + contains: func(key: string) -> result; + + /// Value byte length, none if the key is absent, without + /// transferring the value. On some backends this may be a scan. + len: func(key: string) -> result, fault>; + + /// Number of keys matching a prefix, without materialising the key + /// list. On some backends this may be a scan. + count: func(prefix: string) -> result; +} diff --git a/shepherd/wit/deps/nexum-host/logging.wit b/shepherd/wit/deps/nexum-host/logging.wit new file mode 100644 index 00000000..8cd98140 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/logging.wit @@ -0,0 +1,15 @@ +package nexum:host@0.1.0; + +interface logging { + enum level { + trace, + debug, + info, + warn, + error, + } + + /// Emit a structured log message. + /// The host decides how to handle it (stdout, file, discard). + log: func(level: level, message: string); +} diff --git a/shepherd/wit/deps/nexum-host/messaging.wit b/shepherd/wit/deps/nexum-host/messaging.wit new file mode 100644 index 00000000..30777ca7 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/messaging.wit @@ -0,0 +1,19 @@ +package nexum:host@0.1.0; + +interface messaging { + use types.{fault, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, fault>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/query-module.wit b/shepherd/wit/deps/nexum-host/query-module.wit new file mode 100644 index 00000000..ffb29b87 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/query-module.wit @@ -0,0 +1,25 @@ +package nexum:host@0.1.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, fault}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, fault>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/remote-store.wit b/shepherd/wit/deps/nexum-host/remote-store.wit new file mode 100644 index 00000000..39b36bae --- /dev/null +++ b/shepherd/wit/deps/nexum-host/remote-store.wit @@ -0,0 +1,33 @@ +package nexum:host@0.1.0; + +interface remote-store { + use types.{fault}; + + /// Upload raw data to the decentralised store. + /// Returns the 32-byte content reference (Swarm address). + upload: func(data: list) -> result, fault>; + + /// Download raw data by 32-byte content reference. + download: func(reference: list) -> result, fault>; + + /// Read the latest value from a mutable feed. + /// + /// Feeds are mutable pointers: (owner, topic) -> latest chunk. + /// `owner`: 20-byte Ethereum address of the feed owner. + /// `topic`: 32-byte topic hash. + read-feed: func( + owner: list, + topic: list, + ) -> result>, fault>; + + /// Update a mutable feed with new data. + /// + /// The host signs the feed update with its configured identity. + /// `topic`: 32-byte topic hash. + /// `data`: the payload to publish. + /// Returns the 32-byte reference of the new chunk. + write-feed: func( + topic: list, + data: list, + ) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/types.wit b/shepherd/wit/deps/nexum-host/types.wit new file mode 100644 index 00000000..8e834263 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/types.wit @@ -0,0 +1,106 @@ +package nexum:host@0.1.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC. +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + /// One decoded log, mirroring the RPC `eth_getLogs` shape field for + /// field so the guest can rebuild the native alloy log losslessly. + /// Fixed-width byte fields are carried raw: `address` is 20 bytes, + /// each topic and hash is 32. The block-scoped fields are absent on a + /// pending log (mined logs carry them all). + record chain-log { + address: list, + topics: list>, + data: list, + block-hash: option>, + block-number: option, + block-timestamp: option, + transaction-hash: option>, + transaction-index: option, + log-index: option, + removed: bool, + } + + /// A batch of logs delivered from one subscription. The alloy log type + /// carries no chain id, so it sits here once: every log in a delivery + /// shares the chain of the subscription that produced it. + record chain-logs { + chain-id: chain-id, + logs: list, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + /// A host-observed status transition for a previously submitted + /// intent. Transport-blind: the subscriber sees only where the + /// intent is in its life, never how the host learnt it. + record intent-status-update { + /// Venue id the receipt was issued by. + venue: string, + /// The venue-scoped intent identifier, opaque to the host. + receipt: list, + /// Opaque versioned status body: a leading u8 version tag, + /// then that version's borsh payload (v1: lifecycle status, + /// optional settlement proof, optional failure reason). An + /// unknown tag is a decode error and the body is never empty. + /// The host never inspects it. + status: list, + } + + variant event { + block(block), + chain-logs(chain-logs), + tick(tick), + message(message), + intent-status(intent-status-update), + } + + /// Opaque config from module.toml [config] section. + type config = list>; + + /// The cross-domain failure vocabulary richer interfaces embed as a + /// case. Each payload-bearing case carries a human-readable detail; + /// `rate-limited` carries structured backoff guidance instead. + variant fault { + unsupported(string), + unavailable(string), + denied(string), + rate-limited(rate-limit), + timeout, + invalid-input(string), + internal(string), + } + + /// Backoff guidance for a `fault.rate-limited`. `retry-after-ms` is + /// the host's suggested wait before retrying, when known. + record rate-limit { + retry-after-ms: option, + } +} diff --git a/shepherd/wit/deps/videre-types/types.wit b/shepherd/wit/deps/videre-types/types.wit new file mode 100644 index 00000000..e8b69c81 --- /dev/null +++ b/shepherd/wit/deps/videre-types/types.wit @@ -0,0 +1,83 @@ +package videre:types@0.1.0; + +/// The venue-neutral intent ontology. Depends only on value-flow; never on +/// nexum:host, so the venue-error transport cases are its own. +interface types { + use videre:value-flow/types@0.1.0.{asset-amount}; + + /// How an intent is authorised at its venue. Non-EVM schemes are 0.2+. + variant auth-scheme { + eip1271, + eip712, + } + + /// Where a deal settles. EVM-only in 0.1. + record settlement { + chain: u64, + } + + /// Adapter-derived description of an intent body: the ontology guard policy + /// runs on. Policy has teeth on `gives`; `wants` is display-grade. + record intent-header { + gives: asset-amount, + wants: asset-amount, + settlement: settlement, + authorisation: auth-scheme, + } + + /// Venue-scoped stable id for a submitted intent. Opaque to host and policy. + type receipt = list; + + /// An EVM call the host must sign and send. The adapter only describes it; + /// the host fills gas/fee and signs, so adapters cannot move value. Always + /// a call to existing code. + record unsigned-tx { + chain: u64, + /// 20-byte contract address. + to: list, + /// Native value, big-endian minimal; empty is zero. + value: list, + /// ABI-encoded calldata. + data: list, + } + + /// What a successful submit produced. + variant submit-outcome { + accepted(receipt), + requires-signing(unsigned-tx), + } + + /// Lifecycle state. Coarse and portable; proof and failure reason ride the + /// opaque status body (docs/design/videre-wit-pinned-0.1.0.md). + enum intent-status { + pending, + open, + fulfilled, + cancelled, + expired, + } + + /// Failure of a client or adapter call. `denied` and `rate-limited` are the + /// only guard/transport shapes; `denied` MUST NOT be retried. + variant venue-error { + unknown-venue, + invalid-body(string), + unsupported, + denied(string), + rate-limited(rate-limit), + unavailable(string), + timeout, + } + + record rate-limit { + retry-after-ms: option, + } + + /// An indicative quotation for a body. Firm/RFQ maker-side offers are 0.2+. + record quotation { + gives: asset-amount, + wants: asset-amount, + fee: asset-amount, + valid-until-ms: u64, + } +} diff --git a/shepherd/wit/deps/videre-value-flow/types.wit b/shepherd/wit/deps/videre-value-flow/types.wit new file mode 100644 index 00000000..c3aa9de9 --- /dev/null +++ b/shepherd/wit/deps/videre-value-flow/types.wit @@ -0,0 +1,32 @@ +package videre:value-flow@0.1.0; + +/// Egress-neutral vocabulary for value in motion. Carries no dependency so it +/// outlives any contract built on it. EVM-only in 0.1. +interface types { + /// 20-byte EVM address, big-endian. + type address = list; + + /// Unsigned integer, big-endian, minimal-length: no leading zero bytes, + /// zero is the empty list. Decoders MUST compare by integer value, not by + /// byte equality. + type uint = list; + + /// An ERC-20 token on the intent's settlement chain. + record erc20 { + token: address, + } + + /// A kind of value that can move. erc721/erc1155/service/offchain are 0.2+. + variant asset { + /// The settlement chain's gas token. + native, + erc20(erc20), + } + + /// An amount of one asset. Never negative; direction lives in the field + /// that holds the pair (`gives` vs `wants`). + record asset-amount { + asset: asset, + amount: uint, + } +} diff --git a/shepherd/wit/deps/videre-venue/venue.wit b/shepherd/wit/deps/videre-venue/venue.wit new file mode 100644 index 00000000..8a9685f2 --- /dev/null +++ b/shepherd/wit/deps/videre-venue/venue.wit @@ -0,0 +1,49 @@ +package videre:venue@0.1.0; + +/// Worker (keeper) face. The host holds the venue registry; the keeper names +/// a venue by string. +interface client { + use videre:types/types@0.1.0.{quotation, receipt, intent-status, submit-outcome, venue-error}; + + quote: func(venue: string, body: list) -> result; + submit: func(venue: string, body: list) -> result; + /// Put an externally-obtained receipt (e.g. an on-chain placement) + /// under the host's status watch; an accepted submit is watched + /// implicitly. Idempotent. + observe: func(venue: string, receipt: receipt) -> result<_, venue-error>; + status: func(venue: string, receipt: receipt) -> result; + cancel: func(venue: string, receipt: receipt) -> result<_, venue-error>; +} + +/// Provider (venue) face. Mirrors `client` without the venue selector: one +/// installed adapter answers for exactly one venue, so the registry resolves +/// a venue id to its adapter and calls it directly. +interface adapter { + use videre:types/types@0.1.0.{intent-header, quotation, receipt, intent-status, submit-outcome, venue-error}; + + /// Body-schema versions this adapter decodes. Must equal the + /// manifest `[venue] body_versions` set; install asserts it. + body-versions: func() -> list; + + /// Pure: derive the guard-facing header from a body. No I/O. + derive-header: func(body: list) -> result; + quote: func(body: list) -> result; + submit: func(body: list) -> result; + status: func(receipt: receipt) -> result; + cancel: func(receipt: receipt) -> result<_, venue-error>; +} + +/// A venue adapter component: the provider face over scoped transport only. +/// No local-store, remote-store, identity, or logging import, so an adapter +/// structurally cannot touch host key material or persistent state. Outbound +/// HTTP is wasi:http, linked separately and allowlisted per adapter. +world venue-adapter { + use nexum:host/types@0.1.0.{config, fault}; + + import nexum:host/chain@0.1.0; + import nexum:host/messaging@0.1.0; + + /// Configure the adapter from its `[config]` before any submission. + export init: func(config: config) -> result<_, fault>; + export adapter; +} diff --git a/shepherd/wit/nexum-host b/shepherd/wit/nexum-host deleted file mode 120000 index 9b9eefce..00000000 --- a/shepherd/wit/nexum-host +++ /dev/null @@ -1 +0,0 @@ -../../nexum/wit/nexum-host \ No newline at end of file diff --git a/shepherd/wit/videre-types b/shepherd/wit/videre-types deleted file mode 120000 index efc63dbb..00000000 --- a/shepherd/wit/videre-types +++ /dev/null @@ -1 +0,0 @@ -../../videre/wit/videre-types \ No newline at end of file diff --git a/shepherd/wit/videre-value-flow b/shepherd/wit/videre-value-flow deleted file mode 120000 index a5d467ee..00000000 --- a/shepherd/wit/videre-value-flow +++ /dev/null @@ -1 +0,0 @@ -../../videre/wit/videre-value-flow \ No newline at end of file diff --git a/shepherd/wit/videre-venue b/shepherd/wit/videre-venue deleted file mode 120000 index b992a9eb..00000000 --- a/shepherd/wit/videre-venue +++ /dev/null @@ -1 +0,0 @@ -../../videre/wit/videre-venue \ No newline at end of file