ruvector/scripts/rvforge-parity-check.sh
rUv 86062a2e13
feat(rvforge): create command, authoring core, and Reader install/library/update (#800)
* feat(forge-core): author module for writing signed RVF containers

rvf-forge-core could verify containers but not produce them, so every
test and fixture had to hand-assemble bytes through testkit. The
author module makes writing a first-class operation: ContainerBuilder
assembles segments, computes per-segment digests, and emits a signed
root manifest that this crate's own verifier accepts.

Segment kind decides signing policy rather than the caller: a .wasm
payload becomes an executable WASM segment and is signed individually,
anything else becomes an opaque VEC segment. That keeps rule 3 of the
loading contract — unsigned executable segments are rejected by
default — a property of the writer, not something each caller has to
remember to ask for.

The parity fixture generator now builds its input through this module
instead of a bespoke byte layout, so the TypeScript and Rust sides are
compared against a shared definition of what a valid container is.

138 tests, clippy clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01ParP55bZs2iTGEGvpnUecx

* feat(rvforge): add the create command that writes a signed agent.rvf

Closes the gap that made the published 0.1.0 unusable end to end:
init printed "Next: rvforge pack <agent.rvf>" while creating no such
file, so a first-time user's next command failed with FORGE_E_IO and
there was no supported way to produce the input every other command
needs. The only valid .rvf in the repo lived in tests/fixtures, which
is not in the published tarball.

create reads project metadata and declared capabilities from
rvforge.json and signs with the key init --keygen recorded, so the
common case takes no arguments. With no --from it writes a minimal
but complete skeleton — a META segment declaring the requested
capability classes and a signed root MANIFEST — which is enough for
validate, test, pack, publish and build to run. Walking the whole
pipeline before you have a model to put in it is the point.

--from <dir> adds files as segments in sorted order, so the same
input directory produces the same bytes.

init's next-step line now points at create rather than at a file it
does not write.

Verified from an empty directory against the built CLI: init, create,
validate --deep and test all exit 0 on a self-authored artifact.

253 tests.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01ParP55bZs2iTGEGvpnUecx

* feat(reader): install, library and update flows over verified artifacts

Takes rvforge-reader from a verification surface to one that manages
installed agents: install, a library of what is installed, and update
with rollback. Each flow re-verifies rather than trusting the step
before it — an artifact that verified at download is verified again
at install and again at load, because the file on disk between those
points is not the same object the check covered.

The dock bridge keeps the trust boundary the Dock exists to enforce.
Chrome the system owns — trust badge, network indicator, pause — is
populated from SystemOwnedStatus only, and agent-supplied text stays
in AgentProvidedStatus and is sanitized before display. A hostile
agent cannot forge an approved badge or claim it has stopped while
running, because the types do not give it a channel to those fields.

Update binds to lineage: an update whose base identity does not match
the installed artifact is refused rather than applied, and rollback
restores the previous version with its state capsule intact.

189 tests, clippy clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01ParP55bZs2iTGEGvpnUecx

* fix(deps): bump rkyv 0.8.16 to 0.8.18 for RUSTSEC-2026-0233/0234/0235

Three advisories published against rkyv 0.8.16: a use-after-free
during deserialization of crafted archives (RUSTSEC-2026-0233), and
out-of-bounds reads from insufficient archive validation for Rc/Arc
(0235) and hash tables (0234).

rkyv is a workspace-wide dependency of ruvector-core, ruvector-graph,
ruvector-router-core and ruvector-sparse-inference. All three
advisories are deserialization-side, which is where untrusted bytes
arrive, so an ignore entry would be the wrong call even though the
existing audit.toml has that mechanism — .cargo/audit.toml states the
policy directly: anything fixable is fixed via a dependency bump
rather than ignored.

Lockfile only, no manifest change. cargo audit exits 0 and the four
dependent crates check clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01ParP55bZs2iTGEGvpnUecx
2026-08-05 12:52:27 -03:00

136 lines
5.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# CLI ↔ Rust parity check, in two halves.
#
# `npm/packages/rvforge` (TypeScript) writes `.rvf` containers and a local
# registry; `crates/rvf-forge-core` and `crates/rvforge-registry` (Rust) read
# them. Both sides implement the same specs and were built independently — so
# "both pass their own tests" says nothing about whether they interoperate.
#
# **Container parity.** `rvforge create` writes a real, signed `.rvf`, and
# `rvforge-rvf-check` inspects and verifies it through `rvf-forge-core`'s public
# API, with the publisher's public key supplied so the Ed25519 signatures are
# actually checked rather than skipped. Nothing is shared between the two
# implementations but the format itself: the segment layout, the SHAKE-256
# content hashes, the signature footers, and the Level-0 root manifest page all
# have to agree byte for byte or this fails.
#
# **Registry parity.** The run then drives pack → publish against that same
# artifact, publishes a *second* release so it exercises lineage rather than a
# single object, and hands the directory to `rvforge-registry-check`, which
# re-derives every content address, recomputes the Merkle log, re-applies the
# publication rules, and walks the witness chains.
#
# Prints `PARITY OK` and exits 0 when the Rust readers accept what the CLI
# wrote; otherwise prints the violation list and exits non-zero.
#
# Usage: scripts/rvforge-parity-check.sh [--keep]
# --keep leave the temporary registry on disk and print its path
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLI_DIR="$REPO_ROOT/npm/packages/rvforge"
CRATE_DIR="$REPO_ROOT/crates/rvforge-registry"
KEEP=0
[[ "${1:-}" == "--keep" ]] && KEEP=1
for required in "$CLI_DIR/package.json" "$CRATE_DIR/Cargo.toml"; do
if [[ ! -f "$required" ]]; then
echo "parity check needs $required; skipping" >&2
exit 0
fi
done
WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/rvforge-parity.XXXXXX")"
cleanup() {
if [[ "$KEEP" -eq 1 ]]; then
echo "kept: $WORKDIR"
else
rm -rf "$WORKDIR"
fi
}
trap cleanup EXIT
PROJECT_DIR="$WORKDIR/project"
REGISTRY_DIR="$WORKDIR/registry"
mkdir -p "$PROJECT_DIR"
echo "==> building the CLI"
# --workspaces=false: the package sits inside the npm/ workspace root, and a
# plain install resolves the whole workspace and fails EBADPLATFORM on
# platform-pinned siblings.
(cd "$CLI_DIR" && npm install --workspaces=false --silent && npm run build --silent)
CLI="$CLI_DIR/dist/cli.js"
KEY_FILE="$PROJECT_DIR/rvforge-publisher.key"
echo "==> rvforge init --keygen"
# init scaffolds rvforge.json too, so it runs first; the fixture generator then
# replaces that scaffold with a project that passes every pack check.
(cd "$PROJECT_DIR" && node "$CLI" init --keygen --quiet >/dev/null)
node "$REPO_ROOT/scripts/rvforge-parity-fixture.cjs" "$CLI_DIR/dist" "$PROJECT_DIR" >/dev/null
echo "==> rvforge create agent.rvf"
# The fixture no longer ships a synthetic .rvf: the CLI writes the artifact
# under test. A .wasm payload makes it carry an executable segment, so the
# run covers the signed-executable path rather than metadata alone.
mkdir -p "$PROJECT_DIR/payload"
printf '\0asm\1\0\0\0' > "$PROJECT_DIR/payload/agent.wasm"
(cd "$PROJECT_DIR" && node "$CLI" create agent.rvf \
--from payload --key-file "$KEY_FILE" --quiet >/dev/null)
echo "==> rvforge-rvf-check (Rust reads the container the CLI wrote)"
# The public key is passed as hex so the Rust side needs no base64 decoder;
# without it every signature check would record "skipped", which would prove
# nothing about whether the CLI's signatures verify.
PUBLIC_KEY_HEX="$(node -e '
const { readFileSync } = require("node:fs");
const key = JSON.parse(readFileSync(process.argv[1], "utf8"));
process.stdout.write(Buffer.from(key.publicKey, "base64").toString("hex"));
' "$KEY_FILE")"
if ! (cd "$REPO_ROOT" && cargo run --quiet -p rvf-forge-core \
--bin rvforge-rvf-check -- "$PROJECT_DIR/agent.rvf" \
--public-key "$PUBLIC_KEY_HEX"); then
echo
echo "PARITY FAILED — crates/rvf-forge-core rejected the .rvf that" >&2
echo "npm/packages/rvforge wrote; the two disagree about the RVF format" >&2
exit 1
fi
echo "==> rvforge pack agent.rvf"
(cd "$PROJECT_DIR" && node "$CLI" pack agent.rvf --quiet >/dev/null)
echo "==> rvforge publish agent.rvf (1.0.0)"
(cd "$PROJECT_DIR" && node "$CLI" publish agent.rvf \
--registry "$REGISTRY_DIR" --key-file "$KEY_FILE" --quiet >/dev/null)
# A second release is what makes the check meaningful: it exercises the
# predecessor link, the release index as a chain, a two-leaf Merkle tree, and a
# witness receipt that has to name the previous one.
echo "==> rvforge publish agent.rvf (1.1.0)"
node -e '
const { readFileSync, writeFileSync } = require("node:fs");
const path = process.argv[1];
const project = JSON.parse(readFileSync(path, "utf8"));
project.version = "1.1.0";
writeFileSync(path, `${JSON.stringify(project, null, 2)}\n`);
' "$PROJECT_DIR/rvforge.json"
(cd "$PROJECT_DIR" && node "$CLI" publish agent.rvf \
--registry "$REGISTRY_DIR" --key-file "$KEY_FILE" --quiet >/dev/null)
echo "==> rvforge-registry-check (Rust)"
if (cd "$REPO_ROOT" && cargo run --quiet -p rvforge-registry \
--bin rvforge-registry-check -- "$REGISTRY_DIR"); then
echo
echo "PARITY OK — the Rust side reads everything the CLI wrote:"
echo " container rvf-forge-core inspected and verified agent.rvf"
echo " registry rvforge-registry replayed the whole publication log"
exit 0
fi
echo
echo "PARITY FAILED — the violations above are places the CLI and" >&2
echo "crates/rvforge-registry disagree about registry-model.md" >&2
exit 1