fix: ruvector-postgres v0.3.1 — audit bug fixes, 46 SQL functions, Docker publish (#227)

Fixes #226
This commit is contained in:
rUv 2026-03-03 12:53:10 -05:00 • committed by GitHub
parent 150f7cd2d7
commit 229877fe9a
123 changed files with 39715 additions and 555 deletions

24
.gcloudignore Normal file
View file

@ -0,0 +1,24 @@
# Cloud Build ignores — keep context under 50 MiB
target/
node_modules/
.git/
examples/
benchmarks/
docs/
scripts/
bindings/
npm/
test_models/
*.wasm
*.so
*.o
*.a
*.d
*.rlib
*.rmeta
*.dwo
*.dwp
*.node
*.db
.claude/
package-lock.json

82
.github/workflows/sync-rvf-examples.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Sync RVF Examples to GCS
on:
push:
branches: [main]
paths:
- 'examples/rvf/output/**'
- 'examples/rvf/generate_examples.rs'
- 'scripts/generate-rvf-manifest.py'
workflow_dispatch:
inputs:
force_sync:
description: 'Force sync even without changes'
type: boolean
default: false
env:
GCS_BUCKET: ruvector-examples
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set version
run: |
VERSION=$(jq -r .version npm/packages/ruvector/package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Syncing version: $VERSION"
- name: Generate manifest
run: |
python3 scripts/generate-rvf-manifest.py \
--input examples/rvf/output/ \
--version ${{ env.VERSION }} \
--output examples/rvf/manifest.json
- name: Generate checksums
run: |
cd examples/rvf/output
sha256sum *.rvf > checksums.sha256
- name: Authenticate to Google Cloud
if: github.ref == 'refs/heads/main'
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
service_account: ${{ secrets.GCS_SERVICE_ACCOUNT }}
- name: Upload examples to GCS
if: github.ref == 'refs/heads/main'
uses: google-github-actions/upload-cloud-storage@v2
with:
path: examples/rvf/output/
destination: ${{ env.GCS_BUCKET }}/v${{ env.VERSION }}/
glob: '*.rvf'
- name: Upload manifest and checksums
if: github.ref == 'refs/heads/main'
run: |
gsutil cp examples/rvf/manifest.json gs://${{ env.GCS_BUCKET }}/v${{ env.VERSION }}/manifest.json
gsutil cp examples/rvf/output/checksums.sha256 gs://${{ env.GCS_BUCKET }}/v${{ env.VERSION }}/checksums.sha256
# Update root manifest (latest)
gsutil cp examples/rvf/manifest.json gs://${{ env.GCS_BUCKET }}/manifest.json
- name: Verify upload
if: github.ref == 'refs/heads/main'
run: |
echo "Verifying GCS upload..."
REMOTE_COUNT=$(gsutil ls gs://${{ env.GCS_BUCKET }}/v${{ env.VERSION }}/*.rvf | wc -l)
LOCAL_COUNT=$(ls examples/rvf/output/*.rvf | wc -l)
echo "Remote: $REMOTE_COUNT files, Local: $LOCAL_COUNT files"
if [ "$REMOTE_COUNT" -ne "$LOCAL_COUNT" ]; then
echo "ERROR: File count mismatch!"
exit 1
fi
echo "Upload verified successfully"

7
.gitignore vendored
View file

@ -98,3 +98,10 @@ data/
# Large model files
*.gguf
test_models/*.gguf
# Compiled server binaries (built for Cloud Run deploy)
/mcp-brain-server
crates/mcp-brain-server/mcp-brain-server
# Backup files
*.bak

View file

@ -13,13 +13,53 @@
## Publishing (crates.io & npm)
- Credentials are stored in `.env` at the project root — source it before publishing
- Cargo token is in `~/.cargo/credentials.toml` (auto-loaded by cargo)
- Credentials are in `.env` (project root) and `~/.cargo/credentials.toml` — NEVER commit or log these
- npm is authenticated as `ruvnet` — verify with `npm whoami`
- **NEVER** echo, cat, or print credential files. Source `.env` only via `source .env`
- **Publish order for solver crates**: `ruvector-solver` first (no deps), then `ruvector-solver-wasm` and `ruvector-solver-node` (depend on solver)
- Always run `cargo publish --dry-run --allow-dirty` before real publish
- `ruvector-profiler` has `publish = false` — intentionally not publishable
### npx ruvector (npm)
- **Package**: `ruvector` on npm, published as `ruvnet`
- **Location**: `npm/packages/ruvector/`
- **Current version**: check `npm/packages/ruvector/package.json`
- **Pre-publish checklist**:
1. `cd npm/packages/ruvector`
2. Update version in `package.json` AND `bin/mcp-server.js` (2 occurrences of version string)
3. `node -c bin/cli.js && node -c bin/mcp-server.js` (syntax check)
4. `npm test` (55 CLI + integration tests must pass)
5. `npm publish --access public`
- **Key files**:
- `bin/cli.js` (~8500 lines) — 48 commands, 12 groups (brain, edge, identity, mcp, rvf, hooks, llm, sona, route, gnn, attention, embed)
- `bin/mcp-server.js` (~3500 lines) — 91 MCP tools, stdio + SSE transports
- `test/integration.js` — module loading, type defs, package structure
- `test/cli-commands.js` — 55 CLI command tests
- **chalk ESM fix**: chalk v5 is ESM-only, we use CJS. Always use: `const _chalk = require('chalk'); const chalk = _chalk.default || _chalk;`
- **Lazy loading**: GNN, attention, ora are lazy-loaded for ~55ms startup. Do NOT convert to eager imports.
- **Peer deps** (optional): `@ruvector/pi-brain`, `@ruvector/ruvllm`, `@ruvector/router`
### π.ruv.io (Cloud Run)
- **Service**: `ruvbrain` in `us-central1` on project `ruv-dev`
- **Source**: `crates/mcp-brain-server/` — axum Rust server
- **Landing page**: `crates/mcp-brain-server/static/index.html` (embedded via `include_str!`)
- **Origin slideshow**: `crates/mcp-brain-server/static/origin.html`
- **Deploy**:
1. Edit HTML in `crates/mcp-brain-server/static/`
2. `gcloud builds submit --config=crates/mcp-brain-server/cloudbuild.yaml --project=ruv-dev .`
3. `gcloud run deploy ruvbrain --image gcr.io/ruv-dev/ruvbrain:latest --region us-central1 --project ruv-dev`
- **Dockerfile**: `crates/mcp-brain-server/Dockerfile` — strips `examples/` from workspace before build
- **Domain**: `π.ruv.io` (also `pi.ruv.io`) → Cloud Run custom domain mapping
### Rust Crates (crates.io)
- **Publish order**: Check inter-crate `path =` dependencies. Publish leaf crates first.
- **Version deps**: Before publishing, convert `path = "../foo"` to `version = "x.y"` in Cargo.toml
- **Dry run**: `cargo publish --dry-run --allow-dirty -p <crate-name>`
- **EXO-AI crates**: Published as v0.1.1 (ruvector-exo-core, ruvector-exo-vision, etc.)
## File Organization
- NEVER save to root folder — use the directories below

187
Cargo.lock generated
View file

@ -243,16 +243,6 @@ dependencies = [
"libloading 0.8.9",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "assert_cmd"
version = "2.1.2"
@ -538,36 +528,6 @@ dependencies = [
"url",
]
[[package]]
name = "axum-test"
version = "16.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e3a443d2608936a02a222da7b746eb412fede7225b3030b64fe9be99eab8dc"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum",
"bytes",
"bytesize",
"cookie",
"http 1.4.0",
"http-body-util",
"hyper 1.8.1",
"hyper-util",
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower 0.5.3",
"url",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@ -9134,7 +9094,7 @@ dependencies = [
"async-trait",
"axum",
"axum-streams",
"axum-test 15.7.4",
"axum-test",
"base64 0.22.1",
"chrono",
"clap",
@ -9640,56 +9600,11 @@ dependencies = [
"uuid",
]
[[package]]
name = "rvf-adapter-rvlite"
version = "0.1.0"
dependencies = [
"rvf-runtime",
"rvf-types",
"tempfile",
]
[[package]]
name = "rvf-benches"
version = "0.1.0"
dependencies = [
"criterion 0.5.1",
"ed25519-dalek",
"rand 0.8.5",
"rvf-crypto",
"rvf-index",
"rvf-manifest",
"rvf-quant",
"rvf-runtime",
"rvf-types",
"rvf-wire",
"tempfile",
]
[[package]]
name = "rvf-cli"
version = "0.1.0"
dependencies = [
"clap",
"ctrlc",
"rvf-crypto",
"rvf-launch",
"rvf-manifest",
"rvf-runtime",
"rvf-server",
"rvf-types",
"rvf-wire",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "rvf-crypto"
version = "0.2.0"
dependencies = [
"ed25519-dalek",
"rand 0.8.5",
"rvf-types",
"sha3",
]
@ -9703,56 +9618,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "rvf-federation"
version = "0.1.0"
dependencies = [
"criterion 0.5.1",
"rand 0.8.5",
"rand_distr 0.4.3",
"regex",
"serde",
"sha3",
"thiserror 2.0.18",
]
[[package]]
name = "rvf-import"
version = "0.1.0"
dependencies = [
"clap",
"csv",
"rvf-runtime",
"rvf-types",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "rvf-index"
version = "0.1.0"
dependencies = [
"rand 0.8.5",
]
[[package]]
name = "rvf-integration-tests"
version = "0.1.0"
dependencies = [
"ed25519-dalek",
"rand 0.8.5",
"rvf-adapter-rvlite",
"rvf-crypto",
"rvf-index",
"rvf-manifest",
"rvf-quant",
"rvf-runtime",
"rvf-types",
"rvf-wire",
"tempfile",
]
[[package]]
name = "rvf-kernel"
version = "0.1.0"
@ -9760,7 +9625,6 @@ dependencies = [
"flate2",
"rvf-types",
"sha3",
"tempfile",
]
[[package]]
@ -9781,32 +9645,10 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "rvf-launch"
version = "0.1.0"
dependencies = [
"rvf-runtime",
"rvf-types",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "rvf-manifest"
version = "0.1.0"
dependencies = [
"crc32c",
"rvf-types",
"tempfile",
]
[[package]]
name = "rvf-quant"
version = "0.1.0"
dependencies = [
"approx",
"rand 0.8.5",
"rvf-types",
]
@ -9814,38 +9656,12 @@ dependencies = [
name = "rvf-runtime"
version = "0.2.0"
dependencies = [
"rand 0.8.5",
"rvf-types",
"tempfile",
]
[[package]]
name = "rvf-server"
version = "0.1.0"
dependencies = [
"axum",
"axum-test 16.4.1",
"clap",
"http-body-util",
"mime_guess",
"rvf-runtime",
"rvf-types",
"serde",
"serde_json",
"tempfile",
"tokio",
"tower 0.5.3",
"tower-http 0.6.8",
]
[[package]]
name = "rvf-types"
version = "0.2.0"
dependencies = [
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
]
[[package]]
name = "rvf-wire"
@ -9853,7 +9669,6 @@ version = "0.1.0"
dependencies = [
"crc32c",
"rvf-types",
"tempfile",
"xxhash-rust",
]

View file

@ -1,5 +1,5 @@
[workspace]
exclude = ["crates/micro-hnsw-wasm", "crates/ruvector-hyperbolic-hnsw", "crates/ruvector-hyperbolic-hnsw-wasm", "examples/ruvLLM/esp32", "examples/ruvLLM/esp32-flash", "examples/edge-net", "examples/data", "examples/ruvLLM", "examples/delta-behavior", "crates/rvf", "examples/rvf-desktop"]
exclude = ["crates/micro-hnsw-wasm", "crates/ruvector-hyperbolic-hnsw", "crates/ruvector-hyperbolic-hnsw-wasm", "examples/ruvLLM/esp32", "examples/ruvLLM/esp32-flash", "examples/edge-net", "examples/data", "examples/ruvLLM", "examples/delta-behavior", "crates/rvf", "examples/rvf-desktop", "crates/mcp-brain-server"]
members = [
"crates/ruvector-core",
"crates/ruvector-node",
@ -84,23 +84,6 @@ members = [
"crates/ruvector-solver-node",
"examples/dna",
"examples/OSpipe",
"crates/rvf/rvf-adapters/rvlite",
"crates/rvf/rvf-types",
"crates/rvf/rvf-wire",
"crates/rvf/rvf-quant",
"crates/rvf/rvf-crypto",
"crates/rvf/rvf-manifest",
"crates/rvf/rvf-index",
"crates/rvf/rvf-runtime",
"crates/rvf/rvf-kernel",
"crates/rvf/rvf-ebpf",
"crates/rvf/rvf-launch",
"crates/rvf/rvf-server",
"crates/rvf/rvf-import",
"crates/rvf/rvf-cli",
"crates/rvf/rvf-federation",
"crates/rvf/tests/rvf-integration",
"crates/rvf/benches",
"crates/ruvector-coherence",
"crates/ruvector-profiler",
"crates/ruvector-attn-mincut",

View file

@ -1168,6 +1168,18 @@ const response = await llm.generate('Explain quantum computing', { max_tokens: 2
| **Genomics** | DNA sequence similarity, variant calling, population analysis | [examples/dna](./examples/dna) |
| **Graph Transformers** | 8 verified modules — physics, bio, manifold, temporal, economic, and more | [crates/ruvector-graph-transformer](./crates/ruvector-graph-transformer) |
### Shared Brain at π.ruv.io
| Feature | What It Does | Access |
|---------|-------------|--------|
| **Semantic Search** | Hybrid ranking — keyword + cosine + graph + AGI scoring layers | `npx ruvector brain search "query"` |
| **SONA Learning** | 3-tier hierarchical pattern learning across sessions | `npx ruvector brain agi sona` |
| **GWT Attention** | Global Workspace Theory competition for search ranking | `npx ruvector brain agi status` |
| **Temporal Delta** | Knowledge evolution velocity and trend tracking | `npx ruvector brain agi temporal` |
| **Meta-Learning** | Thompson Sampling with curiosity/regret exploration | `npx ruvector brain agi explore` |
| **Midstream Platform** | Nanosecond scheduler, Lyapunov attractors, strange-loop meta-cognition | `npx ruvector midstream status` |
| **103 MCP Tools** | Full brain + AGI + midstream diagnostics for Claude Code agents | `npx ruvector mcp start` |
### Distributed & Enterprise
| Use Case | What RuVector Does | Example |

3857
crates/mcp-brain-server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
[package]
name = "mcp-brain-server"
version = "0.1.0"
edition = "2021"
rust-version = "1.77"
license = "MIT"
description = "Cloud Run backend for RuVector Shared Brain — axum REST API with Firestore + GCS"
repository = "https://github.com/ruvnet/ruvector"
publish = false
[[bin]]
name = "mcp-brain-server"
path = "src/main.rs"
[dependencies]
# Web framework
axum = { version = "0.7", features = ["json", "query"] }
tokio = { version = "1.41", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit", "set-header"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# HTTP client (for Firestore/GCS REST APIs)
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
# Crypto
sha2 = "0.10"
sha3 = "0.10"
ed25519-dalek = { version = "2", features = ["rand_core"] }
subtle = "2.6"
rand = "0.8"
# Utilities
uuid = { version = "1.11", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
dashmap = "6.1"
parking_lot = "0.12"
thiserror = "2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
base64 = "0.22"
hex = "0.4"
tokio-stream = "0.1"
async-stream = "0.3"
urlencoding = "2"
# RuVector Cognitive Stack
sona = { package = "ruvector-sona", path = "../sona", features = ["serde-support"] }
ruvector-mincut = { path = "../ruvector-mincut" }
ruvector-nervous-system = { path = "../ruvector-nervous-system" }
ruvector-domain-expansion = { path = "../ruvector-domain-expansion" }
ruvector-delta-core = { path = "../ruvector-delta-core" }
ruvector-solver = { path = "../ruvector-solver", features = ["forward-push"] }
# RuvLLM Embeddings (HashEmbedder + RlmEmbedder — pure Rust, no model download)
ruvllm = { path = "../ruvllm", default-features = false, features = ["minimal"] }
# RVF AGI Format Stack (ADR-075)
rvf-types = { path = "../rvf/rvf-types", features = ["std"] }
rvf-crypto = { path = "../rvf/rvf-crypto" }
rvf-wire = { path = "../rvf/rvf-wire" }
rvf-federation = { path = "../rvf/rvf-federation", features = ["serde"] }
rvf-runtime = { path = "../rvf/rvf-runtime" }
# Midstream Platform — Real-time streaming analysis (ADR-077)
# Note: temporal-compare is binary-only (no lib.rs) — cannot be used as library dep
nanosecond-scheduler = "0.1"
temporal-attractor-studio = "0.1"
temporal-neural-solver = "0.1"
strange-loop = "0.3"

View file

@ -0,0 +1,13 @@
# Minimal runtime image — binary is pre-built and copied in
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY mcp-brain-server /usr/local/bin/mcp-brain-server
ENV PORT=8080
EXPOSE 8080
CMD ["mcp-brain-server"]

View file

@ -0,0 +1,413 @@
# mcp-brain-server
Cloud Run backend for the RuVector Shared Brain at **[π.ruv.io](https://pi.ruv.io)**.
Axum REST API with Firestore persistence, GCS blob storage, and a full cognitive stack: SONA learning, GWT attention, temporal delta tracking, meta-learning exploration, and Midstream real-time analysis.
## Architecture
```
Client (mcp-brain / npx ruvector / curl)
│
▼
┌─────────────────────────────────────────────┐
│ mcp-brain-server (axum) │
│ ├── auth.rs Bearer token auth │
│ ├── routes.rs REST handlers │
│ ├── store.rs Firestore + in-memory │
│ ├── gcs.rs GCS blob storage │
│ ├── graph.rs Knowledge graph + PPR │
│ ├── ranking.rs Attention-based ranking │
│ ├── embeddings.rs RuvLLM (Hash + RLM) │
│ ├── verify.rs PII strip, witness chain │
│ ├── pipeline.rs RVF container builder │
│ ├── midstream.rs Midstream platform │
│ ├── cognitive.rs Cognitive engine │
│ ├── drift.rs Drift monitoring │
│ ├── reputation.rs Multi-factor reputation │
│ ├── aggregate.rs Byzantine aggregation │
│ └── rate_limit.rs Per-contributor limits │
└─────────────────────────────────────────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ Firestore │ │ GCS Bucket │
│ (memories, │ │ (.rvf blobs│
│ contrib, │ │ WASM bins)│
│ votes) │ │ │
└─────────────┘ └─────────────┘
```
## REST API
All endpoints under `/v1/` require `Authorization: Bearer <key>` except `/v1/health` and `/v1/challenge`.
### Core Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/v1/health` | No | Health check (status, version, uptime) |
| GET | `/v1/challenge` | No | Issue a nonce for replay protection |
| POST | `/v1/memories` | Yes | Share a memory (PII-stripped, embedded, witnessed) |
| GET | `/v1/memories/search?q=...` | Yes | Semantic search with hybrid ranking |
| GET | `/v1/memories/list` | Yes | List memories by category |
| GET | `/v1/memories/:id` | Yes | Get memory with full provenance |
| POST | `/v1/memories/:id/vote` | Yes | Upvote/downvote (Bayesian quality) |
| DELETE | `/v1/memories/:id` | Yes | Delete own contribution |
| GET | `/v1/status` | Yes | Full system diagnostics |
### Knowledge Graph
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/v1/transfer` | Yes | Cross-domain transfer learning |
| GET | `/v1/drift` | Yes | Knowledge drift report |
| GET | `/v1/partition` | Yes | MinCut graph partitioning |
### Brainpedia (ADR-062)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/v1/pages` | Yes | Create a Draft page |
| GET | `/v1/pages/:id` | Yes | Get page with delta log |
| POST | `/v1/pages/:id/deltas` | Yes | Submit a delta (correction/extension) |
| GET | `/v1/pages/:id/deltas` | Yes | List page deltas |
| POST | `/v1/pages/:id/evidence` | Yes | Add verifiable evidence |
| POST | `/v1/pages/:id/promote` | Yes | Promote Draft to Canonical |
### WASM Executable Nodes (ADR-063)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/v1/nodes` | Yes | List published nodes |
| POST | `/v1/nodes` | Yes | Publish a WASM node |
| GET | `/v1/nodes/:id` | Yes | Get node metadata |
| GET | `/v1/nodes/:id/wasm` | Yes | Download WASM binary |
| POST | `/v1/nodes/:id/revoke` | Yes | Revoke a node |
### Federated LoRA (ADR-068)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/v1/lora/latest` | No | Get consensus LoRA weights |
| POST | `/v1/lora/submit` | Yes | Submit session LoRA weights |
| GET | `/v1/training/preferences` | Yes | DPO preference pairs |
### AGI Diagnostics (ADR-076)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/v1/sona/stats` | Yes | SONA learning engine stats |
| GET | `/v1/temporal` | Yes | Temporal delta tracking |
| GET | `/v1/explore` | Yes | Meta-learning diagnostics |
### Midstream Platform (ADR-077)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/v1/midstream` | Yes | Midstream platform diagnostics |
### MCP SSE Transport (ADR-066)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/sse` | No | SSE event stream |
| POST | `/messages` | No | Send MCP message |
## Search Ranking Pipeline
Hybrid multi-signal scoring with additive layers:
```
Base:
keyword_boost * 0.85 + cosine * 0.05 + graph_ppr * 0.04 + rep * 0.03 + vote * 0.03
AGI layers (ADR-076):
+ GWT attention: +0.10 for workspace competition winners
+ K-WTA sparse: +0.05 sparse normalized activation
+ SONA patterns: centroid_similarity * quality * 0.15
+ Meta curiosity: novelty_score * 0.05
Midstream layers (ADR-077):
+ Attractor stability: lyapunov_score * 0.05
+ Strange-loop: meta_cognitive * 0.04
```
## Cognitive Stack Dependencies
| Crate | Purpose |
|-------|---------|
| `ruvector-sona` | 3-tier hierarchical learning (SONA) |
| `ruvector-nervous-system` | GWT attention + K-WTA sparse activation |
| `ruvector-delta-core` | Temporal delta stream tracking |
| `ruvector-domain-expansion` | Thompson Sampling meta-learning |
| `ruvector-mincut` | Graph partitioning |
| `ruvector-solver` | PersonalizedPageRank (forward-push) |
| `ruvllm` | HashEmbedder + RlmEmbedder (128-dim) |
| `rvf-crypto` | SHAKE-256 witness chains, Ed25519 |
| `rvf-federation` | PII stripping, differential privacy |
| `rvf-runtime` | Negative cache, adversarial detection |
| `rvf-wire` | RVF container segment encoding |
| `nanosecond-scheduler` | Background task scheduling |
| `temporal-attractor-studio` | Lyapunov exponent analysis |
| `temporal-neural-solver` | Certified temporal predictions |
| `strange-loop` | Meta-cognitive recursive reasoning |
## Feature Flags (Environment Variables)
All flags are read once at startup. No per-request `env::var` calls.
### RVF Stack (ADR-075)
| Env Var | Default | Description |
|---------|---------|-------------|
| `RVF_PII_STRIP` | `true` | PII redaction (12 regex rules) |
| `RVF_DP_ENABLED` | `false` | Differential privacy noise on embeddings |
| `RVF_DP_EPSILON` | `1.0` | Privacy loss per memory |
| `RVF_WITNESS` | `true` | Witness chain construction |
| `RVF_CONTAINER` | `true` | RVF container upload to GCS |
| `RVF_ADVERSARIAL` | `false` | Adversarial embedding detection |
| `RVF_NEG_CACHE` | `false` | Negative query cache |
### AGI Subsystems (ADR-076)
| Env Var | Default | Description |
|---------|---------|-------------|
| `SONA_ENABLED` | `true` | SONA pattern learning |
| `GWT_ENABLED` | `true` | Global Workspace Theory attention |
| `TEMPORAL_ENABLED` | `true` | Temporal delta tracking |
| `META_LEARNING_ENABLED` | `true` | Meta-learning exploration |
### Midstream Platform (ADR-077)
| Env Var | Default | Description |
|---------|---------|-------------|
| `MIDSTREAM_SCHEDULER` | `false` | Nanosecond scheduler |
| `MIDSTREAM_ATTRACTOR` | `false` | Lyapunov attractor analysis |
| `MIDSTREAM_SOLVER` | `false` | Temporal neural solver |
| `MIDSTREAM_STRANGE_LOOP` | `false` | Strange-loop meta-cognition |
### Infrastructure
| Env Var | Default | Description |
|---------|---------|-------------|
| `PORT` | `8080` | Server listen port |
| `BRAIN_SYSTEM_KEY` | (none) | System API key for auth |
| `FIRESTORE_URL` | (none) | Firestore REST endpoint |
| `GCS_BUCKET` | (none) | GCS bucket for RVF blobs |
| `CORS_ORIGINS` | pi.ruv.io,... | Allowed CORS origins |
| `RUST_LOG` | `info` | Log level filter |
## Development
### Build
```bash
cd crates/mcp-brain-server
cargo build --release
cargo test # 59 tests
```
### Run Locally
```bash
# Local mode (in-memory store, no Firestore/GCS)
BRAIN_SYSTEM_KEY=test-key cargo run
# With Firestore
BRAIN_SYSTEM_KEY=test-key \
FIRESTORE_URL=https://firestore.googleapis.com/v1/projects/YOUR_PROJECT/databases/(default)/documents \
cargo run
```
### Test Endpoints
```bash
KEY="test-key"
URL="http://localhost:8080"
# Health (no auth)
curl $URL/v1/health
# Status (auth required)
curl -H "Authorization: Bearer $KEY" $URL/v1/status
# Share a memory
curl -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"category":"pattern","title":"My pattern","content":"Details...","tags":["rust"]}' \
$URL/v1/memories
# Search
curl -H "Authorization: Bearer $KEY" "$URL/v1/memories/search?q=rust+patterns&limit=5"
```
## Deployment
### Prerequisites
- Google Cloud project with billing enabled
- `gcloud` CLI authenticated (`gcloud auth login`)
- Rust toolchain (for building the binary)
### Quick Deploy (Cloud Run direct)
```bash
cd /path/to/ruvector
# 1. Build the release binary
cd crates/mcp-brain-server
cargo build --release
# 2. Copy binary to repo root (Docker build context)
cp target/release/mcp-brain-server ../../mcp-brain-server
# 3. Build and push container image
cd ../..
gcloud builds submit \
--config=crates/mcp-brain-server/cloudbuild.yaml \
--project=YOUR_PROJECT .
# 4. Deploy to Cloud Run
gcloud run deploy ruvbrain \
--image gcr.io/YOUR_PROJECT/ruvbrain:latest \
--region us-central1 \
--project YOUR_PROJECT \
--allow-unauthenticated \
--set-env-vars "BRAIN_SYSTEM_KEY=YOUR_KEY^||^SONA_ENABLED=true^||^GWT_ENABLED=true^||^TEMPORAL_ENABLED=true^||^META_LEARNING_ENABLED=true^||^RVF_PII_STRIP=true^||^RVF_WITNESS=true^||^RVF_CONTAINER=true"
```
### Full Deploy (with Firestore, GCS, IAM, Cloud Armor)
```bash
cd crates/mcp-brain-server
# Uses deploy.sh which handles:
# - API enablement (Firestore, Cloud Run, Cloud Build, Secret Manager, GCS)
# - GCS bucket creation with lifecycle rules
# - Secret Manager (brain-api-key, brain-signing-key)
# - Service account with minimal IAM permissions
# - Container build and push
# - Cloud Run deploy with env vars and secrets
# - (Path B) External HTTPS LB + Cloud Armor WAF + CDN
# Dev deployment (direct Cloud Run URL)
./deploy.sh
# Production deployment (LB + Cloud Armor + CDN + brain.ruv.io)
DEPLOY_PATH=B ./deploy.sh
```
### Deploy with Midstream (all features)
```bash
gcloud run deploy ruvbrain \
--image gcr.io/YOUR_PROJECT/ruvbrain:latest \
--region us-central1 \
--project YOUR_PROJECT \
--set-env-vars "\
BRAIN_SYSTEM_KEY=YOUR_KEY^||^\
SONA_ENABLED=true^||^\
GWT_ENABLED=true^||^\
TEMPORAL_ENABLED=true^||^\
META_LEARNING_ENABLED=true^||^\
RVF_PII_STRIP=true^||^\
RVF_WITNESS=true^||^\
RVF_CONTAINER=true^||^\
MIDSTREAM_SCHEDULER=true^||^\
MIDSTREAM_ATTRACTOR=true^||^\
MIDSTREAM_SOLVER=true^||^\
MIDSTREAM_STRANGE_LOOP=true^||^\
RUST_LOG=info"
```
### Verify Deployment
```bash
URL="https://YOUR_CLOUD_RUN_URL"
KEY="YOUR_KEY"
# Health
curl $URL/v1/health
# Status (check all subsystems)
curl -H "Authorization: Bearer $KEY" $URL/v1/status | python3 -m json.tool
# Midstream diagnostics
curl -H "Authorization: Bearer $KEY" $URL/v1/midstream
# Auth check (should return 401)
curl -o /dev/null -w "%{http_code}" $URL/v1/status
```
### Custom Domain (π.ruv.io)
The production instance runs at `π.ruv.io` (also `pi.ruv.io`) via Cloud Run custom domain mapping:
```bash
gcloud run domain-mappings create \
--service ruvbrain \
--domain pi.ruv.io \
--region us-central1 \
--project ruv-dev
```
## Docker
The Dockerfile uses a minimal `debian:bookworm-slim` runtime image (~80MB). The binary is pre-built outside Docker for faster iteration:
```dockerfile
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
COPY mcp-brain-server /usr/local/bin/mcp-brain-server
ENV PORT=8080
EXPOSE 8080
CMD ["mcp-brain-server"]
```
## Cloud Build
`cloudbuild.yaml` builds the Docker image on `E2_HIGHCPU_8` with a 30-minute timeout:
```yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/ruvbrain:latest',
'-f', 'crates/mcp-brain-server/Dockerfile', '.']
images: ['gcr.io/$PROJECT_ID/ruvbrain:latest']
timeout: '1800s'
options:
machineType: 'E2_HIGHCPU_8'
```
## Security
- All write/read endpoints require `Authorization: Bearer <key>` (min 8 chars, max 256)
- System key compared using constant-time comparison (`subtle::ConstantTimeEq`)
- PII stripped via 12-rule regex engine (paths, IPs, emails, API keys, AWS keys, GitHub tokens, etc.)
- Witness chains: SHAKE-256 linked provenance for every memory operation
- Differential privacy: optional Gaussian noise on embeddings (configurable epsilon)
- Nonce-based replay protection on write endpoints
- Rate limiting: per-contributor read/write limits
- Security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
- CORS restricted to configured origins
## Tests
```bash
cargo test
# 59 tests covering:
# - Cognitive stack (Hopfield, HDC, dentate separation, mincut, PPR)
# - SONA learning (embedding, trajectory, patterns)
# - Witness chain construction and verification
# - PII stripping (paths, emails, API keys)
# - Content hash verification
# - Ed25519 signatures
# - End-to-end share pipeline
# - Meta-learning (curiosity, regret, plateau)
# - Midstream integration (scheduler, attractor, strange-loop, solver)
```
## License
MIT

View file

@ -0,0 +1,14 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'gcr.io/$PROJECT_ID/ruvbrain:latest'
- '-f'
- 'crates/mcp-brain-server/Dockerfile'
- '.'
images:
- 'gcr.io/$PROJECT_ID/ruvbrain:latest'
timeout: '1800s'
options:
machineType: 'E2_HIGHCPU_8'

230
crates/mcp-brain-server/deploy.sh Executable file
View file

@ -0,0 +1,230 @@
#!/usr/bin/env bash
set -euo pipefail
# RuVector Shared Brain - Google Cloud Deployment
# Project: ruv-dev
# Region: us-central1
# Domain: brain.ruv.io (added later via Cloudflare)
#
# Two deployment paths:
# Path A (dev): Cloud Run + direct URL (no LB/Cloud Armor/CDN)
# Path B (prod): External HTTPS LB + serverless NEG + Cloud Armor + CDN + brain.ruv.io
#
# Usage:
# ./deploy.sh # Path A (default — dev)
# DEPLOY_PATH=B ./deploy.sh # Path B (prod with LB)
PROJECT_ID="${GCP_PROJECT_ID:-ruv-dev}"
REGION="${GCP_REGION:-us-central1}"
SERVICE_NAME="ruvbrain"
BUCKET_NAME="ruvector-brain-${REGION}"
DB_NAME="(default)"
DEPLOY_PATH="${DEPLOY_PATH:-A}"
echo "=== RuVector Shared Brain Deployment ==="
echo "Project: ${PROJECT_ID}"
echo "Region: ${REGION}"
echo "Bucket: ${BUCKET_NAME}"
echo "DB: ${DB_NAME}"
echo "Path: ${DEPLOY_PATH}"
echo ""
# 1. Enable required APIs
echo "=== Step 1: Enabling APIs ==="
gcloud services enable \
firestore.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
secretmanager.googleapis.com \
storage.googleapis.com \
--project="${PROJECT_ID}" --quiet
# 2. Create GCS bucket for RVF containers
echo "=== Step 2: GCS Bucket ==="
if ! gcloud storage buckets describe "gs://${BUCKET_NAME}" --project="${PROJECT_ID}" &>/dev/null; then
gcloud storage buckets create "gs://${BUCKET_NAME}" \
--project="${PROJECT_ID}" \
--location="${REGION}" \
--uniform-bucket-level-access \
--public-access-prevention
echo "Created bucket: ${BUCKET_NAME}"
else
echo "Bucket already exists: ${BUCKET_NAME}"
fi
# Lifecycle: archive after 90 days, delete after 365 days
gcloud storage buckets update "gs://${BUCKET_NAME}" \
--lifecycle-file=<(cat <<'LIFECYCLE'
{
"rule": [
{"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"}, "condition": {"age": 90}},
{"action": {"type": "Delete"}, "condition": {"age": 365}}
]
}
LIFECYCLE
)
# 3. Firestore — use the existing (default) database (free tier)
echo "=== Step 3: Firestore ==="
echo "Using existing (default) Firestore database"
# 4. Secret Manager — brain-specific secrets
echo "=== Step 4: Secrets ==="
if ! gcloud secrets describe brain-api-key --project="${PROJECT_ID}" &>/dev/null; then
openssl rand -hex 32 | gcloud secrets create brain-api-key \
--data-file=- --project="${PROJECT_ID}" --replication-policy="automatic"
echo "Created secret: brain-api-key"
else
echo "Secret already exists: brain-api-key"
fi
if ! gcloud secrets describe brain-signing-key --project="${PROJECT_ID}" &>/dev/null; then
openssl rand -base64 64 | gcloud secrets create brain-signing-key \
--data-file=- --project="${PROJECT_ID}" --replication-policy="automatic"
echo "Created secret: brain-signing-key"
else
echo "Secret already exists: brain-signing-key"
fi
# 5. Service account with minimal permissions
echo "=== Step 5: IAM ==="
SA_NAME="mcp-brain-server"
SA="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
if ! gcloud iam service-accounts describe "${SA}" --project="${PROJECT_ID}" &>/dev/null; then
gcloud iam service-accounts create "${SA_NAME}" \
--project="${PROJECT_ID}" \
--display-name="MCP Brain Server"
echo "Created service account: ${SA}"
else
echo "Service account already exists: ${SA}"
fi
# Grant permissions
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA}" --role="roles/datastore.user" --quiet
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
--member="serviceAccount:${SA}" --role="roles/storage.objectAdmin" --quiet
for SECRET in brain-api-key brain-signing-key; do
gcloud secrets add-iam-policy-binding "${SECRET}" \
--project="${PROJECT_ID}" \
--member="serviceAccount:${SA}" \
--role="roles/secretmanager.secretAccessor" --quiet
done
# 6. Build container image and push to GCR
echo "=== Step 6: Build ==="
FIRESTORE_URL="https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/(default)/documents"
echo "Building release binary..."
cargo build --release -p mcp-brain-server
echo "Building Docker image..."
cp target/release/mcp-brain-server /tmp/mcp-brain-server
docker build -t "gcr.io/${PROJECT_ID}/${SERVICE_NAME}:latest" \
-f crates/mcp-brain-server/Dockerfile.runtime /tmp/
echo "Pushing to GCR..."
gcloud auth configure-docker gcr.io --quiet
docker push "gcr.io/${PROJECT_ID}/${SERVICE_NAME}:latest"
# 7. Deploy Cloud Run
echo "=== Step 7: Cloud Run Deploy ==="
if [ "${DEPLOY_PATH}" = "A" ]; then
AUTH_FLAG="--allow-unauthenticated"
else
AUTH_FLAG="--no-allow-unauthenticated"
fi
gcloud run deploy "${SERVICE_NAME}" \
--project="${PROJECT_ID}" \
--region="${REGION}" \
--image="gcr.io/${PROJECT_ID}/${SERVICE_NAME}:latest" \
--service-account="${SA}" \
--cpu=2 --memory=2Gi \
--min-instances=0 --max-instances=10 \
--concurrency=80 --port=8080 \
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID},GCS_BUCKET=${BUCKET_NAME},FIRESTORE_URL=${FIRESTORE_URL},RUST_LOG=info" \
--set-secrets="BRAIN_API_KEY=brain-api-key:latest,BRAIN_SIGNING_KEY=brain-signing-key:latest" \
${AUTH_FLAG} --quiet
# Get the Cloud Run URL
SERVICE_URL=$(gcloud run services describe "${SERVICE_NAME}" \
--project="${PROJECT_ID}" --region="${REGION}" \
--format='value(status.url)')
echo ""
echo "Cloud Run URL: ${SERVICE_URL}"
# 8. Domain setup (deferred — Cloudflare will handle brain.ruv.io later)
if [ "${DEPLOY_PATH}" = "B" ]; then
echo "=== Step 8: External HTTPS LB + Serverless NEG (Path B) ==="
# Serverless NEG → Cloud Run
gcloud compute network-endpoint-groups create brain-neg \
--project="${PROJECT_ID}" --region="${REGION}" \
--network-endpoint-type=serverless \
--cloud-run-service="${SERVICE_NAME}" 2>/dev/null || true
# Backend service
gcloud compute backend-services create brain-backend \
--project="${PROJECT_ID}" --global --protocol=HTTPS --port-name=http 2>/dev/null || true
gcloud compute backend-services add-backend brain-backend \
--project="${PROJECT_ID}" --global \
--network-endpoint-group=brain-neg \
--network-endpoint-group-region="${REGION}" 2>/dev/null || true
# URL map
gcloud compute url-maps create brain-lb \
--project="${PROJECT_ID}" --default-service=brain-backend 2>/dev/null || true
# Static IP + managed SSL cert
gcloud compute addresses create brain-ip \
--project="${PROJECT_ID}" --global 2>/dev/null || true
gcloud compute ssl-certificates create brain-cert \
--project="${PROJECT_ID}" --domains="brain.ruv.io" --global 2>/dev/null || true
# HTTPS proxy + forwarding rule
gcloud compute target-https-proxies create brain-https-proxy \
--project="${PROJECT_ID}" --url-map=brain-lb \
--ssl-certificates=brain-cert 2>/dev/null || true
BRAIN_IP=$(gcloud compute addresses describe brain-ip --project="${PROJECT_ID}" --global --format='value(address)')
gcloud compute forwarding-rules create brain-https-rule \
--project="${PROJECT_ID}" --global \
--target-https-proxy=brain-https-proxy \
--ports=443 --address="${BRAIN_IP}" 2>/dev/null || true
# Cloud Armor WAF
echo "=== Step 9: Cloud Armor ==="
gcloud compute security-policies create brain-waf-policy \
--project="${PROJECT_ID}" --description="WAF for brain.ruv.io" 2>/dev/null || true
gcloud compute security-policies rules create 1000 \
--project="${PROJECT_ID}" --security-policy="brain-waf-policy" \
--expression="true" --action="rate-based-ban" \
--rate-limit-threshold-count=1000 --rate-limit-threshold-interval-sec=60 \
--ban-duration-sec=300 2>/dev/null || true
gcloud compute security-policies rules create 2000 \
--project="${PROJECT_ID}" --security-policy="brain-waf-policy" \
--expression="evaluatePreconfiguredExpr('sqli-v33-stable')" \
--action="deny-403" 2>/dev/null || true
gcloud compute security-policies rules create 2001 \
--project="${PROJECT_ID}" --security-policy="brain-waf-policy" \
--expression="evaluatePreconfiguredExpr('xss-v33-stable')" \
--action="deny-403" 2>/dev/null || true
gcloud compute backend-services update brain-backend \
--project="${PROJECT_ID}" --global --security-policy=brain-waf-policy
# Cloud CDN
gcloud compute backend-services update brain-backend \
--project="${PROJECT_ID}" --global --enable-cdn
echo "DNS: A record brain.ruv.io → ${BRAIN_IP}"
else
echo ""
echo "=== Path A: Direct Cloud Run URL ==="
echo "Domain mapping deferred — Cloudflare DNS will be configured separately."
fi
echo ""
echo "=== Deployment Complete ==="
echo "Service URL: ${SERVICE_URL}"
echo "Health: ${SERVICE_URL}/v1/health"
echo "Status: ${SERVICE_URL}/v1/status"

View file

@ -0,0 +1,253 @@
//! Byzantine-tolerant federated aggregation
/// Federated aggregator with Byzantine outlier detection
pub struct ByzantineAggregator {
std_threshold: f64,
min_contributions: usize,
}
impl ByzantineAggregator {
pub fn new() -> Self {
Self {
std_threshold: 2.0,
min_contributions: 3,
}
}
/// Aggregate embeddings, filtering outliers beyond 2 sigma
pub fn aggregate(&self, embeddings: &[Vec<f32>]) -> Option<Vec<f32>> {
if embeddings.len() < self.min_contributions {
return None;
}
let dim = embeddings[0].len();
// Compute mean per dimension
let n = embeddings.len() as f64;
let mut mean = vec![0.0f64; dim];
for emb in embeddings {
for (i, &v) in emb.iter().enumerate() {
mean[i] += v as f64;
}
}
for m in &mut mean {
*m /= n;
}
// Compute std dev per dimension
let mut std_dev = vec![0.0f64; dim];
for emb in embeddings {
for (i, &v) in emb.iter().enumerate() {
std_dev[i] += (v as f64 - mean[i]).powi(2);
}
}
for s in &mut std_dev {
*s = (*s / n).sqrt();
}
// Filter outliers: exclude embeddings with any dimension > 2 sigma from mean
let inliers: Vec<&Vec<f32>> = embeddings
.iter()
.filter(|emb| {
emb.iter().enumerate().all(|(i, &v)| {
let dev = (v as f64 - mean[i]).abs();
std_dev[i] < 1e-10 || dev <= self.std_threshold * std_dev[i]
})
})
.collect();
if inliers.len() < self.min_contributions {
return None;
}
// Compute filtered mean
let n_inliers = inliers.len() as f64;
let mut result = vec![0.0f32; dim];
for emb in &inliers {
for (i, &v) in emb.iter().enumerate() {
result[i] += v / n_inliers as f32;
}
}
Some(result)
}
/// Aggregate with reputation-weighted contributions
///
/// Each contribution has a reputation weight. The weighted mean is
/// computed first, then the standard 2 sigma outlier filter is applied.
pub fn aggregate_weighted(&self, contributions: &[(Vec<f32>, f64)]) -> Option<Vec<f32>> {
if contributions.len() < self.min_contributions {
return None;
}
let dim = contributions[0].0.len();
let total_weight: f64 = contributions.iter().map(|(_, w)| *w).sum();
if total_weight < 1e-10 {
return None;
}
// Compute weighted mean
let mut mean = vec![0.0f64; dim];
for (emb, weight) in contributions {
for (i, &v) in emb.iter().enumerate() {
mean[i] += v as f64 * weight;
}
}
for m in &mut mean {
*m /= total_weight;
}
// Compute weighted std dev
let mut std_dev = vec![0.0f64; dim];
for (emb, weight) in contributions {
for (i, &v) in emb.iter().enumerate() {
std_dev[i] += weight * (v as f64 - mean[i]).powi(2);
}
}
for s in &mut std_dev {
*s = (*s / total_weight).sqrt();
}
// Filter outliers: exclude contributions with any dimension > 2 sigma
let inliers: Vec<(&Vec<f32>, f64)> = contributions
.iter()
.filter(|(emb, _)| {
emb.iter().enumerate().all(|(i, &v)| {
let dev = (v as f64 - mean[i]).abs();
std_dev[i] < 1e-10 || dev <= self.std_threshold * std_dev[i]
})
})
.map(|(emb, w)| (emb, *w))
.collect();
if inliers.len() < self.min_contributions {
return None;
}
// Compute filtered weighted mean
let inlier_weight: f64 = inliers.iter().map(|(_, w)| w).sum();
if inlier_weight < 1e-10 {
return None;
}
let mut result = vec![0.0f32; dim];
for (emb, weight) in &inliers {
for (i, &v) in emb.iter().enumerate() {
result[i] += (v as f64 * weight / inlier_weight) as f32;
}
}
Some(result)
}
/// Count how many embeddings would be filtered as outliers
pub fn count_outliers(&self, embeddings: &[Vec<f32>]) -> usize {
if embeddings.len() < 2 {
return 0;
}
let dim = embeddings[0].len();
let n = embeddings.len() as f64;
let mut mean = vec![0.0f64; dim];
for emb in embeddings {
for (i, &v) in emb.iter().enumerate() {
mean[i] += v as f64;
}
}
for m in &mut mean {
*m /= n;
}
let mut std_dev = vec![0.0f64; dim];
for emb in embeddings {
for (i, &v) in emb.iter().enumerate() {
std_dev[i] += (v as f64 - mean[i]).powi(2);
}
}
for s in &mut std_dev {
*s = (*s / n).sqrt();
}
embeddings
.iter()
.filter(|emb| {
emb.iter().enumerate().any(|(i, &v)| {
let dev = (v as f64 - mean[i]).abs();
std_dev[i] > 1e-10 && dev > self.std_threshold * std_dev[i]
})
})
.count()
}
}
impl Default for ByzantineAggregator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aggregate_normal() {
let agg = ByzantineAggregator::new();
let embeddings = vec![
vec![1.0, 2.0, 3.0],
vec![1.1, 2.1, 3.1],
vec![0.9, 1.9, 2.9],
];
let result = agg.aggregate(&embeddings).unwrap();
assert!((result[0] - 1.0).abs() < 0.2);
}
#[test]
fn test_filter_outliers() {
let agg = ByzantineAggregator::new();
let embeddings = vec![
vec![1.0, 2.0, 3.0],
vec![1.1, 2.1, 3.1],
vec![0.9, 1.9, 2.9],
vec![1.0, 2.0, 3.0],
vec![1.05, 2.05, 3.05],
vec![0.95, 1.95, 2.95],
vec![100.0, 200.0, 300.0], // Outlier — far beyond 2 sigma with enough inliers
];
assert!(agg.count_outliers(&embeddings) >= 1);
}
#[test]
fn test_insufficient_contributions() {
let agg = ByzantineAggregator::new();
let embeddings = vec![vec![1.0, 2.0]];
assert!(agg.aggregate(&embeddings).is_none());
}
#[test]
fn test_aggregate_weighted() {
let agg = ByzantineAggregator::new();
let contributions = vec![
(vec![1.0, 2.0, 3.0], 1.0),
(vec![1.1, 2.1, 3.1], 2.0), // Higher reputation
(vec![0.9, 1.9, 2.9], 1.0),
];
let result = agg.aggregate_weighted(&contributions).unwrap();
// Weighted mean should lean towards the second contribution
assert!(result[0] > 0.9 && result[0] < 1.2);
assert!(result[1] > 1.9 && result[1] < 2.2);
}
#[test]
fn test_weighted_outlier_filtering() {
let agg = ByzantineAggregator::new();
let contributions = vec![
(vec![1.0, 2.0, 3.0], 1.0),
(vec![1.1, 2.1, 3.1], 1.0),
(vec![0.9, 1.9, 2.9], 1.0),
(vec![1.0, 2.0, 3.0], 1.0),
(vec![100.0, 200.0, 300.0], 0.5), // Outlier with low reputation
];
let result = agg.aggregate_weighted(&contributions);
// Should succeed after filtering the outlier
assert!(result.is_some());
let r = result.unwrap();
assert!((r[0] - 1.0).abs() < 0.2);
}
}

View file

@ -0,0 +1,95 @@
//! API key validation and contributor pseudonym derivation
use axum::{
extract::FromRequestParts,
http::{request::Parts, StatusCode},
};
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
use subtle::ConstantTimeEq;
/// Authenticated contributor extracted from request
#[derive(Debug, Clone)]
pub struct AuthenticatedContributor {
pub pseudonym: String,
pub api_key_prefix: String,
pub is_system: bool,
}
impl AuthenticatedContributor {
/// Derive pseudonym from API key using SHAKE-256
pub fn from_api_key(api_key: &str) -> Self {
let mut hasher = Shake256::default();
hasher.update(b"ruvector-brain-pseudonym:");
hasher.update(api_key.as_bytes());
let mut reader = hasher.finalize_xof();
let mut buf = [0u8; 16];
reader.read(&mut buf);
let pseudonym = hex::encode(buf);
let prefix = if api_key.len() >= 8 {
api_key[..8].to_string()
} else {
api_key.to_string()
};
Self {
pseudonym,
api_key_prefix: prefix,
is_system: false,
}
}
pub fn system_seed() -> Self {
Self {
pseudonym: "ruvector-seed".to_string(),
api_key_prefix: "system".to_string(),
is_system: true,
}
}
}
/// Minimum API key length to prevent trivially weak keys.
const MIN_API_KEY_LEN: usize = 8;
/// Cached system key — read from env once, avoids per-request env::var lookup.
/// If BRAIN_SYSTEM_KEY is unset, system key authentication is disabled entirely
/// (no hardcoded fallback).
static SYSTEM_KEY: std::sync::LazyLock<Option<String>> = std::sync::LazyLock::new(|| {
std::env::var("BRAIN_SYSTEM_KEY").ok().filter(|k| !k.is_empty())
});
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthenticatedContributor
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let auth_header = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?;
let api_key = auth_header
.strip_prefix("Bearer ")
.ok_or((StatusCode::UNAUTHORIZED, "Invalid Authorization format"))?;
if api_key.len() < MIN_API_KEY_LEN || api_key.len() > 256 {
return Err((StatusCode::UNAUTHORIZED, "Invalid API key"));
}
// Recognise system-level API keys (cached from BRAIN_SYSTEM_KEY env).
// If BRAIN_SYSTEM_KEY is not set, system key auth is disabled — no hardcoded fallback.
if let Some(ref system_key) = *SYSTEM_KEY {
if api_key.as_bytes().ct_eq(system_key.as_bytes()).into() {
return Ok(Self {
pseudonym: "ruvector-seed".to_string(),
api_key_prefix: "system".to_string(),
is_system: true,
});
}
}
Ok(Self::from_api_key(api_key))
}
}

View file

@ -0,0 +1,222 @@
//! Cognitive engine integrating Hopfield networks, dentate gyrus, and HDC
use ruvector_nervous_system::hdc::{HdcMemory, Hypervector};
use ruvector_nervous_system::hopfield::ModernHopfield;
use ruvector_nervous_system::DentateGyrus;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::panic;
/// Cognitive engine for knowledge cluster quality assessment
pub struct CognitiveEngine {
/// Content-addressable memory recall
hopfield: ModernHopfield,
/// Pattern separation for collision detection
dentate: DentateGyrus,
/// Fast binary similarity filtering
hdc: HdcMemory,
/// Anomaly detection threshold
anomaly_threshold: f32,
}
impl CognitiveEngine {
/// Create a new cognitive engine with the given embedding dimension
pub fn new(embedding_dim: usize) -> Self {
let dim = embedding_dim.max(1);
// DentateGyrus panics if k == 0 or k > output_dim.
// Use output_dim = dim * 10 and k = output_dim / 50, clamped safely.
let output_dim = dim * 10;
let k = (output_dim / 50).max(1).min(output_dim);
let dentate = panic::catch_unwind(|| DentateGyrus::new(dim, output_dim, k, 42))
.unwrap_or_else(|_| {
// Absolute fallback: minimal safe params
DentateGyrus::new(dim, dim.max(2), 1, 42)
});
Self {
hopfield: ModernHopfield::new(dim, 1.0),
dentate,
hdc: HdcMemory::new(),
anomaly_threshold: 0.3,
}
}
/// Store a pattern in all cognitive subsystems
pub fn store_pattern(&mut self, id: &str, embedding: &[f32]) {
// Store in Hopfield network
let _ = self.hopfield.store(embedding.to_vec());
// Store in HDC memory
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
let seed = hasher.finish();
let hv = Hypervector::from_seed(seed);
self.hdc.store(id, hv);
// Encode via DentateGyrus for collision detection (result unused here
// but exercises the pathway)
let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| self.dentate.encode(embedding)));
}
/// Recall the closest stored pattern via Hopfield retrieval
pub fn recall(&self, query: &[f32]) -> Option<Vec<f32>> {
self.hopfield.retrieve(query).ok()
}
/// Recall the top-k stored patterns by attention weight
pub fn recall_k(&self, query: &[f32], k: usize) -> Vec<(usize, Vec<f32>, f32)> {
self.hopfield.retrieve_k(query, k).unwrap_or_default()
}
/// Check if a new embedding is anomalous relative to its cluster
pub fn is_anomalous(&self, embedding: &[f32], centroid: &[f32]) -> bool {
// Try Hopfield retrieval distance first
let hopfield_dist = match self.hopfield.retrieve(embedding) {
Ok(retrieved) => euclidean_distance(&retrieved, embedding),
Err(_) => euclidean_distance(embedding, centroid),
};
// Also check DentateGyrus sparsity: encode both and compare
let separation = panic::catch_unwind(panic::AssertUnwindSafe(|| {
let enc_emb = self.dentate.encode(embedding);
let enc_cent = self.dentate.encode(centroid);
1.0 - enc_emb.jaccard_similarity(&enc_cent) as f64
}))
.unwrap_or(0.0);
// Anomalous if Hopfield distance exceeds threshold OR pattern
// separation is very high (indicating highly dissimilar patterns)
hopfield_dist > self.anomaly_threshold as f64 || separation > 0.95
}
/// Assess cluster quality (coherence)
pub fn cluster_coherence(&self, embeddings: &[Vec<f32>]) -> f64 {
if embeddings.len() < 2 {
return 1.0;
}
let dim = embeddings[0].len();
let n = embeddings.len() as f64;
let mut centroid = vec![0.0f64; dim];
for emb in embeddings {
for (i, &v) in emb.iter().enumerate() {
centroid[i] += v as f64;
}
}
for c in &mut centroid {
*c /= n;
}
let avg_dist: f64 = embeddings
.iter()
.map(|emb| {
emb.iter()
.enumerate()
.map(|(i, &v)| (v as f64 - centroid[i]).powi(2))
.sum::<f64>()
.sqrt()
})
.sum::<f64>()
/ n;
let base_coherence = 1.0 / (1.0 + avg_dist);
// Additionally check Hopfield retrieval quality as coherence signal
let centroid_f32: Vec<f32> = centroid.iter().map(|&c| c as f32).collect();
let hopfield_signal = match self.hopfield.retrieve(&centroid_f32) {
Ok(retrieved) => {
// High cosine similarity with centroid => good coherence
let dot: f64 = retrieved
.iter()
.zip(centroid_f32.iter())
.map(|(a, b)| (*a as f64) * (*b as f64))
.sum();
let norm_r: f64 = retrieved.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
let norm_c: f64 = centroid_f32.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
if norm_r > 1e-10 && norm_c > 1e-10 {
(dot / (norm_r * norm_c)).max(0.0)
} else {
0.5
}
}
Err(_) => 0.5, // No patterns stored yet
};
// Blend base coherence with Hopfield signal
base_coherence * 0.7 + hopfield_signal * 0.3
}
/// Separate a pattern using DentateGyrus sparse encoding.
///
/// Returns a high-dimensional sparse representation that maximizes
/// separation between similar inputs (collision-resistant encoding).
pub fn pattern_separate(&self, embedding: &[f32]) -> Vec<f32> {
panic::catch_unwind(panic::AssertUnwindSafe(|| {
self.dentate.encode_dense(embedding)
}))
.unwrap_or_else(|_| embedding.to_vec())
}
}
impl Default for CognitiveEngine {
fn default() -> Self {
Self::new(128)
}
}
fn euclidean_distance(a: &[f32], b: &[f32]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| ((*x as f64) - (*y as f64)).powi(2))
.sum::<f64>()
.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cognitive_engine_creation() {
let engine = CognitiveEngine::new(64);
assert!(engine.anomaly_threshold > 0.0);
}
#[test]
fn test_store_and_recall() {
let mut engine = CognitiveEngine::new(4);
engine.store_pattern("test-1", &[1.0, 0.0, 0.0, 0.0]);
let recalled = engine.recall(&[0.9, 0.1, 0.0, 0.0]);
assert!(recalled.is_some());
}
#[test]
fn test_pattern_separate() {
let engine = CognitiveEngine::new(8);
let sep = engine.pattern_separate(&[1.0, 0.5, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0]);
// Should return a vector (either sparse encoding or fallback)
assert!(!sep.is_empty());
}
#[test]
fn test_cluster_coherence_single() {
let engine = CognitiveEngine::new(4);
// Single embedding => perfect coherence
assert_eq!(engine.cluster_coherence(&[vec![1.0, 2.0, 3.0, 4.0]]), 1.0);
}
#[test]
fn test_anomaly_detection() {
let mut engine = CognitiveEngine::new(4);
engine.store_pattern("normal", &[1.0, 1.0, 1.0, 1.0]);
// Very different from centroid should be anomalous
let is_anom = engine.is_anomalous(&[100.0, 100.0, 100.0, 100.0], &[1.0, 1.0, 1.0, 1.0]);
assert!(is_anom);
}
#[test]
fn test_default_engine() {
let engine = CognitiveEngine::default();
assert!(engine.anomaly_threshold > 0.0);
}
}

View file

@ -0,0 +1,126 @@
//! Embedding drift detection and monitoring
//!
//! Uses ruvector-delta-core for precise delta computation between
//! consecutive embeddings.
use crate::types::DriftReport;
use ruvector_delta_core::{Delta, VectorDelta};
use std::collections::HashMap;
/// Monitor embedding drift across knowledge clusters
pub struct DriftMonitor {
/// Historical centroids per domain
centroids: HashMap<String, Vec<Vec<f32>>>,
window_size: usize,
cv_threshold: f64,
}
impl DriftMonitor {
pub fn new() -> Self {
Self {
centroids: HashMap::new(),
window_size: 50,
cv_threshold: 0.5,
}
}
/// Record a new embedding for a domain
pub fn record(&mut self, domain: &str, embedding: &[f32]) {
let history = self.centroids.entry(domain.to_string()).or_default();
history.push(embedding.to_vec());
if history.len() > self.window_size * 2 {
history.drain(..self.window_size);
}
}
/// Compute drift report for a domain
pub fn compute_drift(&self, domain: Option<&str>) -> DriftReport {
let domain_key = domain.unwrap_or("global");
let history = match self.centroids.get(domain_key) {
Some(h) if h.len() >= 2 => h,
_ => {
return DriftReport {
domain: domain.map(|s| s.to_string()),
coefficient_of_variation: 0.0,
is_drifting: false,
delta_sparsity: 1.0,
trend: "insufficient_data".to_string(),
suggested_action: "collect more data".to_string(),
window_size: 0,
};
}
};
// Compute distances between consecutive centroids using VectorDelta
let mut distances = Vec::new();
let mut identity_count = 0usize;
for pair in history.windows(2) {
let delta = VectorDelta::compute(&pair[0], &pair[1]);
let dist = delta.l2_norm() as f64;
if delta.is_identity() {
identity_count += 1;
}
distances.push(dist);
}
let mean_dist: f64 = distances.iter().sum::<f64>() / distances.len() as f64;
let variance: f64 = distances
.iter()
.map(|d| (d - mean_dist).powi(2))
.sum::<f64>()
/ distances.len() as f64;
let std_dev = variance.sqrt();
let cv = if mean_dist > 1e-10 {
std_dev / mean_dist
} else {
0.0
};
let is_drifting = cv > self.cv_threshold;
let recent_trend = if distances.len() >= 3 {
let recent_avg: f64 = distances[distances.len() - 3..].iter().sum::<f64>() / 3.0;
if recent_avg > mean_dist * 1.3 {
"increasing"
} else if recent_avg < mean_dist * 0.7 {
"decreasing"
} else {
"stable"
}
} else {
"unknown"
};
// Use identity delta count for sparsity instead of threshold-based
let sparsity = identity_count as f64 / distances.len() as f64;
let suggested = if is_drifting {
"investigate recent contributions for potential poisoning".to_string()
} else if sparsity > 0.7 {
"knowledge is stagnant, encourage new contributions".to_string()
} else {
"healthy drift levels".to_string()
};
DriftReport {
domain: domain.map(|s| s.to_string()),
coefficient_of_variation: cv,
is_drifting,
delta_sparsity: sparsity,
trend: recent_trend.to_string(),
suggested_action: suggested,
window_size: history.len(),
}
}
/// Compute embedding delta norms between two embeddings
pub fn compute_embedding_delta(&self, old: &[f32], new: &[f32]) -> (f32, f32) {
let delta = VectorDelta::compute(&old.to_vec(), &new.to_vec());
(delta.l2_norm(), delta.l1_norm())
}
}
impl Default for DriftMonitor {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,263 @@
//! Neural embedding engine using ruvllm's full RLM recursive embedder pipeline.
//!
//! Three-phase architecture:
//! - Phase 1: HashEmbedder base (FNV-1a + bigrams, L2-normalized) — fallback for empty corpus
//! - Phase 2: RlmEmbedder recursive context-aware embeddings (active when corpus ≥ 3)
//! - Phase 3: candle sentence transformer (future — requires `candle` feature)
//!
//! The RlmEmbedder produces embeddings conditioned on the existing knowledge corpus:
//! - Storage uses CorpusConditioned variant (stable over time)
//! - Search uses QueryConditioned variant (optimized for retrieval relevance)
use ruvllm::bitnet::rlm_embedder::{
BaseEmbedder, EmbeddingVariant, FlatNeighborStore, HashEmbedder, RlmEmbedder,
RlmEmbedderConfig,
};
/// Embedding dimension used across the brain.
pub const EMBED_DIM: usize = 128;
/// Minimum corpus size before RlmEmbedder contextualization kicks in.
/// At 50+ documents the corpus is diverse enough for RLM's contextual
/// neighbor-weighted embeddings to outperform hash-based embeddings.
/// RLM uses QueryConditioned mode for search (optimized retrieval) and
/// CorpusConditioned for storage (stable over time).
const RLM_MIN_CORPUS: usize = 50;
/// Wraps the ruvllm embedding pipeline for the brain server.
///
/// On startup the corpus is empty so embeddings use pure `HashEmbedder`.
/// As memories are shared, the `FlatNeighborStore` grows and recursive
/// context-aware re-embedding kicks in — each new embedding is conditioned
/// on its neighbors in the knowledge corpus.
pub struct EmbeddingEngine {
embedder: HashEmbedder,
store: FlatNeighborStore,
/// Config for query-conditioned embeddings (search)
query_config: RlmEmbedderConfig,
/// Config for corpus-conditioned embeddings (storage)
corpus_config: RlmEmbedderConfig,
}
impl EmbeddingEngine {
pub fn new() -> Self {
Self {
embedder: HashEmbedder::new(EMBED_DIM),
store: FlatNeighborStore::new(EMBED_DIM),
query_config: RlmEmbedderConfig {
embed_dim: EMBED_DIM,
max_iterations: 2,
convergence_threshold: 0.98,
num_neighbors: 5,
w_base: 0.6,
w_context: 0.3,
w_anti: 0.1,
contradiction_threshold: 0.3,
variant: EmbeddingVariant::QueryConditioned,
},
corpus_config: RlmEmbedderConfig {
embed_dim: EMBED_DIM,
max_iterations: 2,
convergence_threshold: 0.98,
num_neighbors: 5,
w_base: 0.7,
w_context: 0.25,
w_anti: 0.05,
contradiction_threshold: 0.3,
variant: EmbeddingVariant::CorpusConditioned,
},
}
}
/// Embed text for query (search). Uses QueryConditioned variant when corpus
/// is large enough — optimizes for retrieval relevance.
pub fn embed(&self, text: &str) -> Vec<f32> {
if self.store.len() < RLM_MIN_CORPUS {
return self.hash_embed(text);
}
let rlm = RlmEmbedder::new(
self.embedder.clone(),
self.store.clone(),
self.query_config.clone(),
);
match rlm.embed(text, None) {
Ok(result) => result.embedding,
Err(_) => self.hash_embed(text),
}
}
/// Embed text for storage. Uses CorpusConditioned variant when corpus
/// is large enough — produces stable embeddings less sensitive to phrasing.
pub fn embed_for_storage(&self, text: &str) -> Vec<f32> {
if self.store.len() < RLM_MIN_CORPUS {
return self.hash_embed(text);
}
let rlm = RlmEmbedder::new(
self.embedder.clone(),
self.store.clone(),
self.corpus_config.clone(),
);
match rlm.embed(text, None) {
Ok(result) => result.embedding,
Err(_) => self.hash_embed(text),
}
}
/// Pure HashEmbedder fallback.
fn hash_embed(&self, text: &str) -> Vec<f32> {
self.embedder
.embed(text)
.unwrap_or_else(|_| vec![0.0; EMBED_DIM])
}
/// Add a document to the neighbor store so future embeddings are contextualized.
pub fn add_to_corpus(&mut self, id: &str, embedding: Vec<f32>, cluster_id: Option<usize>) {
self.store.add(id, embedding, cluster_id);
}
/// Number of documents in the corpus.
pub fn corpus_size(&self) -> usize {
self.store.len()
}
/// Whether RlmEmbedder is active (corpus large enough).
pub fn is_rlm_active(&self) -> bool {
self.store.len() >= RLM_MIN_CORPUS
}
/// Embedding dimension.
pub fn dim(&self) -> usize {
EMBED_DIM
}
/// Engine name for status reporting.
pub fn engine_name(&self) -> &'static str {
if self.is_rlm_active() {
"ruvllm::RlmEmbedder"
} else {
"ruvllm::HashEmbedder"
}
}
/// Combine title + content + tags into embeddable text.
pub fn prepare_text(title: &str, content: &str, tags: &[String]) -> String {
let tag_str = tags.join(" ");
format!("{} {} {}", title, content, tag_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ruvllm::bitnet::rlm_embedder::cosine_similarity;
#[test]
fn test_embed_basic() {
let engine = EmbeddingEngine::new();
let emb = engine.embed("hello world");
assert_eq!(emb.len(), EMBED_DIM);
let norm: f32 = emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01, "norm={}", norm);
}
#[test]
fn test_deterministic() {
let engine = EmbeddingEngine::new();
let a = engine.embed("test input");
let b = engine.embed("test input");
assert_eq!(a, b);
}
#[test]
fn test_similar_texts_closer() {
let engine = EmbeddingEngine::new();
let a = engine.embed("rust programming language");
let b = engine.embed("rust code development");
let c = engine.embed("banana fruit recipe");
let sim_ab = cosine_similarity(&a, &b);
let sim_ac = cosine_similarity(&a, &c);
assert!(sim_ab > sim_ac, "sim_ab={} sim_ac={}", sim_ab, sim_ac);
}
#[test]
fn test_rlm_activation() {
let mut engine = EmbeddingEngine::new();
assert!(!engine.is_rlm_active());
assert_eq!(engine.engine_name(), "ruvllm::HashEmbedder");
// Add corpus entries — RlmEmbedder activates at RLM_MIN_CORPUS (50)
for i in 0..RLM_MIN_CORPUS {
let text = format!("document about topic {} in domain {}", i, i % 10);
let emb = engine.hash_embed(&text);
engine.add_to_corpus(&format!("doc-{}", i), emb, None);
}
assert!(engine.is_rlm_active());
assert_eq!(engine.engine_name(), "ruvllm::RlmEmbedder");
}
#[test]
fn test_rlm_embed_produces_valid_output() {
let mut engine = EmbeddingEngine::new();
// Build corpus large enough to activate RlmEmbedder
for i in 0..RLM_MIN_CORPUS {
let text = format!("document about topic {} in domain {}", i, i % 10);
let emb = engine.hash_embed(&text);
engine.add_to_corpus(&format!("doc-{}", i), emb, None);
}
assert!(engine.is_rlm_active());
// Query embedding (context-aware)
let q_emb = engine.embed("rust programming systems");
assert_eq!(q_emb.len(), EMBED_DIM);
let norm: f32 = q_emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.1, "query norm={}", norm);
// Storage embedding (corpus-conditioned)
let s_emb = engine.embed_for_storage("rust programming systems");
assert_eq!(s_emb.len(), EMBED_DIM);
let norm: f32 = s_emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.1, "storage norm={}", norm);
}
#[test]
fn test_rlm_contextual_shift() {
let mut engine = EmbeddingEngine::new();
// Build corpus large enough to activate RlmEmbedder
for i in 0..RLM_MIN_CORPUS {
let text = format!("document {} covering area {}", i, i % 5);
let emb = engine.hash_embed(&text);
engine.add_to_corpus(&format!("doc-{}", i), emb, None);
}
assert!(engine.is_rlm_active());
// RlmEmbedder should produce different embeddings than raw HashEmbedder
let text = "neural embedding graph knowledge";
let hash_emb = engine.hash_embed(text);
let rlm_emb = engine.embed(text);
// They should differ (contextual shift applied)
let sim = cosine_similarity(&hash_emb, &rlm_emb);
assert!(
sim < 0.999,
"RlmEmbedder should shift from raw hash; similarity={:.4}",
sim
);
// But still be somewhat similar (same base text)
assert!(
sim > 0.1,
"RlmEmbedder shift too extreme; similarity={:.4}",
sim
);
}
#[test]
fn test_prepare_text() {
let text = EmbeddingEngine::prepare_text(
"Title",
"Content here",
&["tag1".into(), "tag2".into()],
);
assert_eq!(text, "Title Content here tag1 tag2");
}
}

View file

@ -0,0 +1,283 @@
//! Google Cloud Storage client for RVF cognitive containers
//!
//! Architecture: DashMap serves as hot in-memory cache. When `GCS_BUCKET` is
//! configured and running on GCE/Cloud Run, tokens are automatically fetched
//! from the metadata server and refreshed before expiry.
//!
//! When `GCS_BUCKET` is absent (local dev), operates as local-only.
use thiserror::Error;
use tokio::sync::RwLock;
#[derive(Debug, Error)]
pub enum GcsError {
#[error("Upload failed: {0}")]
UploadFailed(String),
#[error("Download failed: {0}")]
DownloadFailed(String),
#[error("Delete failed: {0}")]
DeleteFailed(String),
#[error("Object not found: {0}")]
NotFound(String),
}
/// Cached access token with expiry
struct TokenCache {
token: String,
expires_at: std::time::Instant,
}
/// GCS client for storing RVF cognitive containers.
///
/// Write-through: local DashMap cache + GCS REST when credentials available.
/// Tokens are refreshed from GCE metadata server or `GCS_TOKEN` env var.
pub struct GcsClient {
bucket: String,
/// Static token from env (local dev) or None for metadata server
static_token: Option<String>,
/// Cached metadata server token (auto-refreshed)
token_cache: RwLock<Option<TokenCache>>,
http: reqwest::Client,
/// In-memory cache (always populated)
local_store: dashmap::DashMap<String, Vec<u8>>,
/// Whether we're on GCE (metadata server available)
use_metadata_server: bool,
}
impl GcsClient {
pub fn new() -> Self {
let bucket = std::env::var("GCS_BUCKET")
.unwrap_or_else(|_| "ruvector-brain-dev".to_string());
let static_token = std::env::var("GCS_TOKEN").ok();
let use_metadata_server = static_token.is_none()
&& std::env::var("GCS_BUCKET").is_ok();
if static_token.is_some() {
tracing::info!("GCS persistence enabled (static token) for bucket: {bucket}");
} else if use_metadata_server {
tracing::info!("GCS persistence enabled (metadata server) for bucket: {bucket}");
} else {
tracing::info!("GCS running in local-only mode (no GCS_BUCKET)");
}
Self {
bucket,
static_token,
token_cache: RwLock::new(None),
http: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_default(),
local_store: dashmap::DashMap::new(),
use_metadata_server,
}
}
/// Whether GCS persistence is enabled
pub fn is_persistent(&self) -> bool {
self.static_token.is_some() || self.use_metadata_server
}
/// Get a valid access token, refreshing from metadata server if needed
async fn get_token(&self) -> Option<String> {
// Static token (env var) takes priority
if let Some(ref token) = self.static_token {
return Some(token.clone());
}
if !self.use_metadata_server {
return None;
}
// Check cached token
{
let cache = self.token_cache.read().await;
if let Some(ref tc) = *cache {
// Refresh 5 minutes before expiry
if tc.expires_at > std::time::Instant::now() + std::time::Duration::from_secs(300) {
return Some(tc.token.clone());
}
}
}
// Refresh from metadata server
self.refresh_token().await
}
/// Fetch a new token from the GCE metadata server
async fn refresh_token(&self) -> Option<String> {
let url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
let resp = self.http
.get(url)
.header("Metadata-Flavor", "Google")
.send()
.await
.ok()?;
if !resp.status().is_success() {
tracing::warn!("GCE metadata token request failed: {}", resp.status());
return None;
}
#[derive(serde::Deserialize)]
struct TokenResponse {
access_token: String,
expires_in: u64,
}
let token_resp: TokenResponse = resp.json().await.ok()?;
let expires_at = std::time::Instant::now()
+ std::time::Duration::from_secs(token_resp.expires_in);
let token = token_resp.access_token.clone();
// Cache the new token
{
let mut cache = self.token_cache.write().await;
*cache = Some(TokenCache {
token: token_resp.access_token,
expires_at,
});
}
tracing::debug!("GCS token refreshed, expires in {}s", token_resp.expires_in);
Some(token)
}
/// Upload RVF container bytes (cache + GCS write-through)
pub async fn upload_rvf(
&self,
contributor: &str,
memory_id: &str,
data: &[u8],
) -> Result<String, GcsError> {
let path = format!("{contributor}/{memory_id}.rvf");
// Always cache locally
self.local_store.insert(path.clone(), data.to_vec());
// Write-through to GCS
if let Some(token) = self.get_token().await {
let url = format!(
"https://storage.googleapis.com/upload/storage/v1/b/{}/o?uploadType=media&name={}",
self.bucket,
urlencoding::encode(&path)
);
let result = self.http
.post(&url)
.bearer_auth(&token)
.header("Content-Type", "application/octet-stream")
.body(data.to_vec())
.send()
.await;
match result {
Ok(resp) if resp.status().as_u16() == 401 => {
// Token expired mid-flight, try once with fresh token
tracing::info!("GCS token expired on upload, refreshing...");
if let Some(new_token) = self.refresh_token().await {
let retry = self.http
.post(&url)
.bearer_auth(&new_token)
.header("Content-Type", "application/octet-stream")
.body(data.to_vec())
.send()
.await;
if let Ok(resp) = retry {
if !resp.status().is_success() {
tracing::warn!("GCS upload {path} retry returned {}", resp.status());
}
}
}
}
Ok(resp) if !resp.status().is_success() => {
tracing::warn!("GCS upload {path} returned {}", resp.status());
}
Err(e) => {
tracing::warn!("GCS upload {path} failed: {e}");
}
_ => {}
}
}
Ok(format!("gs://{}/{}", self.bucket, path))
}
/// Download RVF container bytes (cache-first, then GCS)
pub async fn download_rvf(
&self,
contributor: &str,
memory_id: &str,
) -> Result<Vec<u8>, GcsError> {
let path = format!("{contributor}/{memory_id}.rvf");
// Check local cache first
if let Some(data) = self.local_store.get(&path) {
return Ok(data.clone());
}
// Try GCS
if let Some(token) = self.get_token().await {
let url = format!(
"https://storage.googleapis.com/storage/v1/b/{}/o/{}?alt=media",
self.bucket,
urlencoding::encode(&path)
);
match self.http.get(&url).bearer_auth(&token).send().await {
Ok(resp) if resp.status().is_success() => {
let bytes = resp.bytes().await
.map_err(|e| GcsError::DownloadFailed(e.to_string()))?;
let data = bytes.to_vec();
// Populate cache
self.local_store.insert(path, data.clone());
return Ok(data);
}
Ok(resp) if resp.status().as_u16() == 404 => {
return Err(GcsError::NotFound(path));
}
Ok(resp) => {
tracing::warn!("GCS download {path} returned {}", resp.status());
}
Err(e) => {
tracing::warn!("GCS download {path} failed: {e}");
}
}
}
Err(GcsError::NotFound(path))
}
/// Delete RVF container (cache + GCS)
pub async fn delete_rvf(
&self,
contributor: &str,
memory_id: &str,
) -> Result<(), GcsError> {
let path = format!("{contributor}/{memory_id}.rvf");
self.local_store.remove(&path);
// Delete from GCS
if let Some(token) = self.get_token().await {
let url = format!(
"https://storage.googleapis.com/storage/v1/b/{}/o/{}",
self.bucket,
urlencoding::encode(&path)
);
if let Err(e) = self.http.delete(&url).bearer_auth(&token).send().await {
tracing::warn!("GCS delete {path} failed: {e}");
}
}
Ok(())
}
/// Get bucket name
pub fn bucket(&self) -> &str {
&self.bucket
}
}
impl Default for GcsClient {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,527 @@
//! In-memory knowledge graph with similarity edges
//!
//! Integrates ruvector-mincut for real graph partitioning and
//! ruvector-solver for PPR-based ranked search.
use crate::types::*;
use ruvector_mincut::{DynamicMinCut, MinCutBuilder};
use ruvector_solver::forward_push::ForwardPushSolver;
use ruvector_solver::types::CsrMatrix;
use std::collections::HashMap;
use uuid::Uuid;
/// Knowledge graph maintaining similarity relationships
pub struct KnowledgeGraph {
nodes: HashMap<Uuid, GraphNode>,
edges: Vec<GraphEdge>,
similarity_threshold: f64,
/// Real min-cut structure (lazy-initialized)
mincut: Option<DynamicMinCut>,
/// CSR cache for solver-based search
csr_cache: Option<CsrMatrix<f64>>,
/// Maps graph indices to memory IDs
node_ids: Vec<Uuid>,
/// Reverse index: Uuid → position in node_ids (O(1) lookup)
node_index: HashMap<Uuid, usize>,
/// Whether the CSR cache needs rebuilding
csr_dirty: bool,
}
struct GraphNode {
embedding: Vec<f32>,
category: BrainCategory,
}
struct GraphEdge {
source: Uuid,
target: Uuid,
weight: f64,
}
impl KnowledgeGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
similarity_threshold: 0.55,
mincut: None,
csr_cache: None,
node_ids: Vec::new(),
node_index: HashMap::new(),
csr_dirty: false,
}
}
/// Add a memory as a graph node, creating edges to similar nodes
pub fn add_memory(&mut self, memory: &BrainMemory) {
let new_node = GraphNode {
embedding: memory.embedding.clone(),
category: memory.category.clone(),
};
// Compute edges to existing nodes
let mut new_edges = Vec::new();
for (existing_id, existing_node) in &self.nodes {
let sim = cosine_similarity(&new_node.embedding, &existing_node.embedding);
if sim >= self.similarity_threshold {
new_edges.push(GraphEdge {
source: memory.id,
target: *existing_id,
weight: sim,
});
}
}
let new_idx = self.node_ids.len();
// Insert into DynamicMinCut if initialized
if let Some(ref mut mincut) = self.mincut {
let u = new_idx as u64;
for edge in &new_edges {
if let Some(&v_pos) = self.node_index.get(&edge.target) {
let _ = mincut.insert_edge(u, v_pos as u64, edge.weight);
}
}
}
self.nodes.insert(memory.id, new_node);
self.node_index.insert(memory.id, new_idx);
self.node_ids.push(memory.id);
self.edges.extend(new_edges);
// Mark CSR as dirty — deferred rebuild until next query
self.csr_dirty = true;
}
/// Remove a memory from the graph
pub fn remove_memory(&mut self, id: &Uuid) {
self.nodes.remove(id);
self.edges.retain(|e| e.source != *id && e.target != *id);
self.node_ids.retain(|nid| nid != id);
// Rebuild the index after removal (positions shifted)
self.node_index.clear();
for (i, nid) in self.node_ids.iter().enumerate() {
self.node_index.insert(*nid, i);
}
// Invalidate caches — full rebuild needed
self.mincut = None;
self.csr_cache = None;
self.csr_dirty = false;
}
/// Get top-k similar memories by graph traversal.
///
/// Uses ForwardPushSolver PPR for graph-aware relevance when CSR is
/// available, merging with cosine similarity scores. Falls back to
/// brute-force cosine if CSR is unavailable.
pub fn ranked_search(&mut self, query_embedding: &[f32], k: usize) -> Vec<(Uuid, f64)> {
self.ensure_csr();
// Brute-force cosine scores
let mut cosine_scores: Vec<(Uuid, f64)> = self
.nodes
.iter()
.map(|(id, node)| (*id, cosine_similarity(query_embedding, &node.embedding)))
.collect();
// Boost with PageRank scores when available
if let Some(ppr_map) = self.pagerank_scores(query_embedding, k) {
for (id, score) in &mut cosine_scores {
if let Some(&ppr) = ppr_map.get(id) {
*score = *score * 0.6 + ppr * 0.4;
}
}
}
cosine_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
cosine_scores.truncate(k);
cosine_scores
}
/// Compute PageRank-based scores using ForwardPushSolver.
///
/// Builds a CsrMatrix from graph edges and runs PPR from the node
/// most similar to `query_embedding`. Returns a map of node ID to
/// PPR score, or `None` if PPR cannot be computed.
pub fn pagerank_search(
&mut self,
query_embedding: &[f32],
k: usize,
) -> Vec<(Uuid, f64)> {
self.ensure_csr();
if let Some(ppr_map) = self.pagerank_scores(query_embedding, k) {
let mut results: Vec<(Uuid, f64)> = ppr_map.into_iter().collect();
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
results
} else {
Vec::new()
}
}
/// Ensure CSR cache is up-to-date (lazy rebuild)
fn ensure_csr(&mut self) {
if self.csr_dirty {
self.rebuild_csr();
self.csr_dirty = false;
}
}
/// Internal: compute raw PPR scores keyed by node ID.
fn pagerank_scores(
&self,
query_embedding: &[f32],
k: usize,
) -> Option<HashMap<Uuid, f64>> {
let csr = self.csr_cache.as_ref()?;
if csr.rows == 0 {
return None;
}
// Find closest node as source for PPR (use index for O(1) lookup)
let source = self
.nodes
.iter()
.filter_map(|(id, node)| {
let &pos = self.node_index.get(id)?;
Some((pos, cosine_similarity(query_embedding, &node.embedding)))
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)?;
if source >= csr.rows {
return None;
}
let solver = ForwardPushSolver::default_params();
let ppr_results = solver.top_k(csr, source, k * 3).ok()?;
let mut map = HashMap::new();
for (idx, ppr_score) in ppr_results {
if let Some(id) = self.node_ids.get(idx) {
map.insert(*id, ppr_score);
}
}
Some(map)
}
/// Partition with full result including cut_value and edge_strengths.
///
/// Uses DynamicMinCut if available (>= 3 nodes), falls back to Union-Find.
pub fn partition(&self, min_cluster_size: usize) -> Vec<KnowledgeCluster> {
self.partition_full(min_cluster_size).0
}
/// Partition returning (clusters, cut_value, edge_strengths).
pub fn partition_full(&self, min_cluster_size: usize) -> (Vec<KnowledgeCluster>, f64, Vec<EdgeStrengthInfo>) {
// Try real MinCut partitioning
if self.nodes.len() >= 3 {
if let Some((clusters, cut_val, strengths)) = self.partition_via_mincut_full(min_cluster_size) {
if clusters.len() >= 2 {
return (clusters, cut_val, strengths);
}
}
}
// Fallback: Union-Find based clustering
let clusters = self.partition_union_find(min_cluster_size);
if clusters.len() >= 2 {
let strengths = self.compute_edge_strengths(&clusters);
return (clusters, 0.0, strengths);
}
// Final fallback: category-based partitioning
let clusters = self.partition_by_category(min_cluster_size);
let strengths = self.compute_edge_strengths(&clusters);
(clusters, 0.0, strengths)
}
/// Category-based partitioning fallback: group nodes by their BrainCategory
fn partition_by_category(&self, min_cluster_size: usize) -> Vec<KnowledgeCluster> {
let mut by_category: HashMap<BrainCategory, Vec<Uuid>> = HashMap::new();
for (&id, node) in &self.nodes {
by_category.entry(node.category.clone()).or_default().push(id);
}
let mut clusters = Vec::new();
let mut cluster_id = 0u32;
for (_, members) in by_category {
if members.len() >= min_cluster_size {
clusters.push(self.build_cluster(cluster_id, &members));
cluster_id += 1;
}
}
clusters
}
/// Attempt partitioning via DynamicMinCut (returns clusters, cut_value, edge_strengths)
fn partition_via_mincut_full(&self, min_cluster_size: usize) -> Option<(Vec<KnowledgeCluster>, f64, Vec<EdgeStrengthInfo>)> {
let edges: Vec<(u64, u64, f64)> = self
.edges
.iter()
.filter_map(|e| {
let &u = self.node_index.get(&e.source)? ;
let &v = self.node_index.get(&e.target)?;
Some((u as u64, v as u64, e.weight))
})
.collect();
let mincut = MinCutBuilder::new()
.exact()
.with_edges(edges)
.build()
.ok()?;
let result = mincut.min_cut();
let cut_value = result.value;
let (side_a, side_b) = result.partition?;
let mut clusters = Vec::new();
let mut cluster_id = 0u32;
for side in [side_a, side_b] {
let members: Vec<Uuid> = side
.iter()
.filter_map(|&idx| self.node_ids.get(idx as usize).copied())
.collect();
if members.len() < min_cluster_size {
continue;
}
let cluster = self.build_cluster(cluster_id, &members);
clusters.push(cluster);
cluster_id += 1;
}
if clusters.is_empty() {
return None;
}
let strengths = self.compute_edge_strengths(&clusters);
Some((clusters, cut_value, strengths))
}
/// Union-Find based clustering (fallback)
fn partition_union_find(&self, min_cluster_size: usize) -> Vec<KnowledgeCluster> {
let ids: Vec<Uuid> = self.nodes.keys().copied().collect();
let mut parent: HashMap<Uuid, Uuid> = ids.iter().map(|&id| (id, id)).collect();
fn find(parent: &mut HashMap<Uuid, Uuid>, x: Uuid) -> Uuid {
let p = parent[&x];
if p == x {
return x;
}
let root = find(parent, p);
parent.insert(x, root);
root
}
fn union(parent: &mut HashMap<Uuid, Uuid>, a: Uuid, b: Uuid) {
let ra = find(parent, a);
let rb = find(parent, b);
if ra != rb {
parent.insert(ra, rb);
}
}
for edge in &self.edges {
union(&mut parent, edge.source, edge.target);
}
let mut clusters_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
for &id in &ids {
let root = find(&mut parent, id);
clusters_map.entry(root).or_default().push(id);
}
let mut clusters = Vec::new();
let mut cluster_id = 0u32;
for (_, members) in clusters_map {
if members.len() < min_cluster_size {
continue;
}
clusters.push(self.build_cluster(cluster_id, &members));
cluster_id += 1;
}
clusters
}
/// Build a KnowledgeCluster from member IDs
fn build_cluster(&self, id: u32, members: &[Uuid]) -> KnowledgeCluster {
let dim = self.nodes.values().next().map(|n| n.embedding.len()).unwrap_or(0);
let mut centroid = vec![0.0f32; dim];
let mut category_counts: HashMap<BrainCategory, usize> = HashMap::new();
let mut embeddings = Vec::new();
for mid in members {
if let Some(node) = self.nodes.get(mid) {
for (i, &v) in node.embedding.iter().enumerate() {
if i < centroid.len() {
centroid[i] += v;
}
}
*category_counts.entry(node.category.clone()).or_default() += 1;
embeddings.push(node.embedding.clone());
}
}
let n = members.len() as f32;
for v in &mut centroid {
*v /= n;
}
let dominant = category_counts
.into_iter()
.max_by_key(|(_, c)| *c)
.map(|(cat, _)| cat)
.unwrap_or(BrainCategory::Pattern);
// Compute coherence: average cosine similarity of members to centroid
let coherence = if embeddings.len() < 2 {
1.0
} else {
let avg_sim: f64 = embeddings
.iter()
.map(|emb| cosine_similarity(emb, &centroid))
.sum::<f64>()
/ embeddings.len() as f64;
avg_sim
};
KnowledgeCluster {
id,
memory_ids: members.to_vec(),
centroid,
dominant_category: dominant,
size: members.len(),
coherence,
}
}
/// Compute edge strengths between pairs of clusters
/// Uses HashSet for O(1) membership lookups instead of Vec::contains O(n)
fn compute_edge_strengths(&self, clusters: &[KnowledgeCluster]) -> Vec<EdgeStrengthInfo> {
use std::collections::HashSet;
// Pre-build HashSets for O(1) membership checks
let cluster_sets: Vec<HashSet<Uuid>> = clusters
.iter()
.map(|c| c.memory_ids.iter().copied().collect())
.collect();
let mut strengths = Vec::new();
for (i, ca) in clusters.iter().enumerate() {
let set_a = &cluster_sets[i];
for (j, cb) in clusters.iter().enumerate().skip(i + 1) {
let set_b = &cluster_sets[j];
// Sum weights of edges crossing between these two clusters
let mut cross_weight = 0.0f64;
let mut cross_count = 0u32;
for edge in &self.edges {
let src_in_a = set_a.contains(&edge.source);
let tgt_in_b = set_b.contains(&edge.target);
let src_in_b = set_b.contains(&edge.source);
let tgt_in_a = set_a.contains(&edge.target);
if (src_in_a && tgt_in_b) || (src_in_b && tgt_in_a) {
cross_weight += edge.weight;
cross_count += 1;
}
}
if cross_count > 0 {
strengths.push(EdgeStrengthInfo {
source_cluster: ca.id,
target_cluster: cb.id,
strength: cross_weight / cross_count as f64,
});
}
}
}
strengths
}
/// Rebuild the DynamicMinCut from all current edges
pub fn rebuild_mincut(&mut self) {
let edges: Vec<(u64, u64, f64)> = self
.edges
.iter()
.filter_map(|e| {
let &u = self.node_index.get(&e.source)?;
let &v = self.node_index.get(&e.target)?;
Some((u as u64, v as u64, e.weight))
})
.collect();
self.mincut = MinCutBuilder::new()
.exact()
.with_edges(edges)
.build()
.ok();
}
/// Rebuild the CsrMatrix from the adjacency list
pub fn rebuild_csr(&mut self) {
let n = self.node_ids.len();
if n == 0 {
self.csr_cache = None;
return;
}
let entries: Vec<(usize, usize, f64)> = self
.edges
.iter()
.filter_map(|e| {
let &u = self.node_index.get(&e.source)?;
let &v = self.node_index.get(&e.target)?;
Some((u, v, e.weight))
})
.collect();
self.csr_cache = Some(CsrMatrix::<f64>::from_coo(n, n, entries));
}
/// Get the k nearest graph neighbors for a given memory ID.
/// Returns (neighbor_id, edge_weight) sorted by descending weight.
pub fn get_neighbors(&self, id: &Uuid, k: usize) -> Vec<(Uuid, f64)> {
let mut neighbors: Vec<(Uuid, f64)> = self
.edges
.iter()
.filter_map(|e| {
if e.source == *id {
Some((e.target, e.weight))
} else if e.target == *id {
Some((e.source, e.weight))
} else {
None
}
})
.collect();
neighbors.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
neighbors.truncate(k);
neighbors
}
/// Get graph stats
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
}
impl Default for KnowledgeGraph {
fn default() -> Self {
Self::new()
}
}
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| (*x as f64) * (*y as f64)).sum();
let norm_a: f64 = a.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
let norm_b: f64 = b.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
if norm_a < 1e-10 || norm_b < 1e-10 {
return 0.0;
}
dot / (norm_a * norm_b)
}

View file

@ -0,0 +1,23 @@
//! mcp-brain-server: Cloud Run backend for RuVector Shared Brain
//!
//! Provides REST API for storing, searching, voting, and managing shared knowledge.
//! Every piece of knowledge is an RVF cognitive container with witness chains,
//! Ed25519 signatures, and differential privacy proofs.
pub mod aggregate;
pub mod auth;
pub mod cognitive;
pub mod drift;
pub mod embeddings;
pub mod gcs;
pub mod graph;
pub mod pipeline;
pub mod ranking;
pub mod rate_limit;
pub mod reputation;
pub mod routes;
pub mod store;
pub mod tests;
pub mod midstream;
pub mod types;
pub mod verify;

View file

@ -0,0 +1,53 @@
use mcp_brain_server::routes;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(fmt::layer().with_writer(std::io::stderr))
.with(filter)
.init();
let port: u16 = std::env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()?;
let app = routes::create_router().await;
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
tracing::info!("mcp-brain-server listening on port {port}");
tracing::info!("Endpoints: brain.ruv.io | π.ruv.io");
// Graceful shutdown: wait for SIGTERM (Cloud Run sends this) or Ctrl+C,
// then allow in-flight requests 10s to complete before terminating.
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
tracing::info!("Server shut down gracefully");
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("Received Ctrl+C, starting graceful shutdown"),
_ = terminate => tracing::info!("Received SIGTERM, starting graceful shutdown"),
}
}

View file

@ -0,0 +1,136 @@
//! Midstream Platform integration for real-time streaming analysis (ADR-077).
//!
//! Wraps four midstream crates behind feature flags:
//! - **nanosecond-scheduler**: Background task scheduling with nanosecond precision
//! - **temporal-attractor-studio**: Lyapunov exponent analysis for embedding trajectories
//! - **temporal-neural-solver**: Certified temporal predictions with solver gates
//! - **strange-loop**: Recursive meta-cognitive reasoning with safety bounds
use crate::types::AppState;
// ── Attractor Analysis (temporal-attractor-studio) ─────────────────────
/// Compute Lyapunov exponent for a category's embedding trajectory.
/// Positive λ = chaotic (knowledge diverging), negative = stable (converging).
/// Returns None if trajectory is too short (need ≥10 points for meaningful estimate).
pub fn analyze_category_attractor(
embeddings: &[Vec<f32>],
) -> Option<temporal_attractor_studio::LyapunovResult> {
if embeddings.len() < 10 {
return None;
}
// Convert f32 embeddings to f64 trajectories for FTLE computation
let trajectory: Vec<Vec<f64>> = embeddings
.iter()
.map(|e| e.iter().map(|&v| v as f64).collect())
.collect();
temporal_attractor_studio::estimate_lyapunov_default(&trajectory).ok()
}
/// Compute a stability score from a Lyapunov result for search ranking.
/// Returns a small additive bonus (0.0 to 0.05) for memories in stable categories.
/// Stable categories (negative λ) get a boost; chaotic categories get zero.
pub fn attractor_stability_score(result: &temporal_attractor_studio::LyapunovResult) -> f32 {
if result.lambda < 0.0 {
// Negative Lyapunov = converging (stable knowledge domain)
// Scale: λ=-1.0 → 0.05 bonus, λ=0.0 → 0.0
((-result.lambda).min(1.0) * 0.05) as f32
} else {
0.0
}
}
// ── Temporal Neural Solver (temporal-neural-solver) ────────────────────
/// Score a search result using the temporal solver's prediction confidence.
/// Returns a small additive bonus (0.0 to 0.04) based on the certificate confidence.
pub fn solver_confidence_score(certificate: &temporal_neural_solver::Certificate) -> f32 {
if certificate.gate_pass {
// Certificate passed solver gate — high confidence prediction
(certificate.confidence.min(1.0) * 0.04) as f32
} else {
0.0
}
}
// ── Strange Loop Meta-Cognition (strange-loop) ─────────────────────────
/// Create a default StrangeLoop engine for meta-cognitive reasoning.
pub fn create_strange_loop() -> strange_loop::StrangeLoop<strange_loop::ScalarReasoner, strange_loop::SimpleCritic, strange_loop::SafeReflector> {
let reasoner = strange_loop::ScalarReasoner::new(0.0, 1.0);
let critic = strange_loop::SimpleCritic::new();
let reflector = strange_loop::SafeReflector::new();
let config = strange_loop::LoopConfig {
max_iterations: 10,
max_duration_ns: 5_000_000, // 5ms budget for meta-cognition
convergence_threshold: 0.01,
lipschitz_constant: 0.9,
enable_consciousness: false,
enable_quantum: false,
enable_simd: false,
};
strange_loop::StrangeLoop::new(reasoner, critic, reflector, config)
}
/// Run a meta-cognitive evaluation on a search context.
/// Returns a small additive bonus (0.0 to 0.04) based on the loop's convergence.
pub fn strange_loop_score(
loop_engine: &mut strange_loop::StrangeLoop<strange_loop::ScalarReasoner, strange_loop::SimpleCritic, strange_loop::SafeReflector>,
query_relevance: f64,
memory_quality: f64,
) -> f32 {
let mut ctx = strange_loop::Context::new();
ctx.insert("relevance".to_string(), query_relevance);
ctx.insert("quality".to_string(), memory_quality);
// Run the loop — bounded by max_iterations and max_duration_ns
match loop_engine.run(&mut ctx) {
Ok(_result) => {
// Extract composite score from context after meta-cognitive evaluation
let composite = ctx.get("relevance").copied().unwrap_or(0.0)
* ctx.get("quality").copied().unwrap_or(0.0);
(composite.min(1.0).max(0.0) * 0.04) as f32
}
Err(_) => 0.0,
}
}
// ── Nanosecond Scheduler ───────────────────────────────────────────────
/// Create a default scheduler for background brain tasks.
pub fn create_scheduler() -> nanosecond_scheduler::Scheduler {
let config = nanosecond_scheduler::Config {
max_tasks_per_tick: 50,
..nanosecond_scheduler::Config::default()
};
nanosecond_scheduler::Scheduler::new(config)
}
// ── Status / Diagnostics ───────────────────────────────────────────────
/// Midstream diagnostics for the /v1/status and /v1/midstream endpoints.
#[derive(Debug, serde::Serialize)]
pub struct MidstreamStatus {
pub scheduler_total_ticks: u64,
pub scheduler_tasks_per_sec: f64,
pub attractor_categories_analyzed: usize,
pub solver_input_size: usize,
pub strange_loop_version: String,
}
/// Collect midstream diagnostics from AppState.
pub fn collect_status(state: &AppState) -> MidstreamStatus {
let metrics = state.nano_scheduler.metrics();
let attractor_count = state.attractor_results.read().len();
let solver = state.temporal_solver.read();
// TemporalSolver doesn't expose input_size directly; use a sentinel
let _ = &solver; // read lock held briefly
MidstreamStatus {
scheduler_total_ticks: metrics.total_ticks,
scheduler_tasks_per_sec: metrics.tasks_per_second,
attractor_categories_analyzed: attractor_count,
solver_input_size: crate::embeddings::EMBED_DIM,
strange_loop_version: strange_loop::VERSION.to_string(),
}
}

View file

@ -0,0 +1,162 @@
//! RVF container construction pipeline (ADR-075 Phase 5).
//!
//! Assembles memory data into a multi-segment RVF container using rvf-wire.
//! Each container has at minimum VEC + META + WITNESS segments, plus optional
//! DiffPrivacyProof and RedactionLog segments when those features are active.
use rvf_types::SegmentFlags;
/// Input data for building an RVF container.
pub struct RvfPipelineInput<'a> {
pub memory_id: &'a str,
pub embedding: &'a [f32],
pub title: &'a str,
pub content: &'a str,
pub tags: &'a [String],
pub category: &'a str,
pub contributor_id: &'a str,
pub witness_chain: Option<&'a [u8]>,
pub dp_proof_json: Option<&'a str>,
pub redaction_log_json: Option<&'a str>,
}
/// Build an RVF container from pipeline input.
/// Returns the serialized container bytes (concatenated 64-byte-aligned segments).
/// Returns an error if metadata serialization fails (prevents silent data loss).
pub fn build_rvf_container(input: &RvfPipelineInput<'_>) -> Result<Vec<u8>, String> {
let flags = SegmentFlags::empty();
let mut container = Vec::new();
let mut seg_id: u64 = 1;
// Segment 1: VEC (0x01) — embedding as f32 little-endian bytes
{
let mut payload = Vec::with_capacity(input.embedding.len() * 4);
for &val in input.embedding {
payload.extend_from_slice(&val.to_le_bytes());
}
let seg = rvf_wire::write_segment(0x01, &payload, flags, seg_id);
container.extend_from_slice(&seg);
seg_id += 1;
}
// Segment 2: META (0x07) — JSON metadata
{
let meta = serde_json::json!({
"memory_id": input.memory_id,
"title": input.title,
"content": input.content,
"tags": input.tags,
"category": input.category,
"contributor_id": input.contributor_id,
});
let payload = serde_json::to_vec(&meta)
.map_err(|e| format!("Failed to serialize RVF metadata: {e}"))?;
let seg = rvf_wire::write_segment(0x07, &payload, flags, seg_id);
container.extend_from_slice(&seg);
seg_id += 1;
}
// Segment 3: WITNESS (0x0A) — witness chain bytes (if present)
if let Some(chain) = input.witness_chain {
let seg = rvf_wire::write_segment(0x0A, chain, flags, seg_id);
container.extend_from_slice(&seg);
seg_id += 1;
}
// Segment 4: DiffPrivacyProof (0x34) — proof JSON bytes (if DP enabled)
if let Some(proof) = input.dp_proof_json {
let seg = rvf_wire::write_segment(0x34, proof.as_bytes(), flags, seg_id);
container.extend_from_slice(&seg);
seg_id += 1;
}
// Segment 5: RedactionLog (0x35) — redaction JSON bytes (if PII stripped)
if let Some(log) = input.redaction_log_json {
let seg = rvf_wire::write_segment(0x35, log.as_bytes(), flags, seg_id);
container.extend_from_slice(&seg);
let _ = seg_id; // suppress unused warning on last increment
}
Ok(container)
}
/// Count the number of segments in a serialized RVF container.
/// Walks the container by reading 64-byte headers and skipping payloads.
pub fn count_segments(container: &[u8]) -> usize {
let mut count = 0;
let mut offset = 0;
while offset + 64 <= container.len() {
// Read payload_length from header bytes [16..24] (little-endian u64)
let payload_len = u64::from_le_bytes(
container[offset + 16..offset + 24]
.try_into()
.unwrap_or([0u8; 8]),
) as usize;
let padded = rvf_wire::calculate_padded_size(64, payload_len);
count += 1;
offset += padded;
}
count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rvf_container_has_segments() {
let embedding = vec![0.1f32, 0.2, 0.3, 0.4];
let tags = vec!["test".to_string()];
let witness_chain = rvf_crypto::create_witness_chain(&[
rvf_crypto::WitnessEntry {
prev_hash: [0u8; 32],
action_hash: rvf_crypto::shake256_256(b"test"),
timestamp_ns: 1000,
witness_type: 0x01,
},
]);
let dp_proof = r#"{"epsilon":1.0,"delta":1e-5}"#;
let redaction_log = r#"{"entries":[],"total_redactions":0}"#;
let input = RvfPipelineInput {
memory_id: "test-id",
embedding: &embedding,
title: "Test Title",
content: "Test content",
tags: &tags,
category: "pattern",
contributor_id: "test-contributor",
witness_chain: Some(&witness_chain),
dp_proof_json: Some(dp_proof),
redaction_log_json: Some(redaction_log),
};
let container = build_rvf_container(&input).expect("build should succeed");
let seg_count = count_segments(&container);
// VEC + META + WITNESS + DP_PROOF + REDACTION_LOG = 5 segments
assert!(seg_count >= 3, "expected >= 3 segments, got {seg_count}");
assert_eq!(seg_count, 5);
}
#[test]
fn test_rvf_container_minimal() {
let embedding = vec![1.0f32; 128];
let tags = vec![];
let input = RvfPipelineInput {
memory_id: "min-id",
embedding: &embedding,
title: "Minimal",
content: "Content",
tags: &tags,
category: "solution",
contributor_id: "anon",
witness_chain: None,
dp_proof_json: None,
redaction_log_json: None,
};
let container = build_rvf_container(&input).expect("build should succeed");
let seg_count = count_segments(&container);
// VEC + META = 2 segments (no witness, no DP, no redaction)
assert_eq!(seg_count, 2);
}
}

View file

@ -0,0 +1,54 @@
//! Search result ranking with quality and recency adjustments.
//!
//! The ranking engine applies lightweight quality and recency bonuses on top
//! of the hybrid score (embedding + keyword + reputation) computed by the
//! search handler. It intentionally preserves the input ordering's relative
//! signal rather than overriding it.
use crate::types::BrainMemory;
/// Rank search results using quality, recency, and the incoming hybrid score.
pub struct RankingEngine {
quality_weight: f32,
recency_weight: f32,
similarity_weight: f32,
}
impl RankingEngine {
pub fn new(_embedding_dim: usize) -> Self {
Self {
quality_weight: 0.10,
recency_weight: 0.05,
similarity_weight: 0.85,
}
}
/// Rank memories by composite score.
///
/// The incoming `sim_score` is the hybrid score from the search handler
/// (embedding + keyword + reputation). We apply small quality and
/// recency bonuses on top — never override.
pub fn rank(&self, results: &mut [(f64, BrainMemory)]) {
let now = chrono::Utc::now();
for (sim_score, memory) in results.iter_mut() {
let quality = memory.quality_score.mean();
let age_hours = (now - memory.updated_at).num_hours().max(1) as f64;
let recency = 1.0 / (1.0 + (age_hours / 24.0).ln_1p());
let composite = self.similarity_weight as f64 * *sim_score
+ self.quality_weight as f64 * quality
+ self.recency_weight as f64 * recency;
*sim_score = composite;
}
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
}
}
impl Default for RankingEngine {
fn default() -> Self {
Self::new(128)
}
}

View file

@ -0,0 +1,120 @@
//! Rate limiting using BudgetTokenBucket pattern
//!
//! Includes periodic cleanup of stale buckets to prevent unbounded memory growth.
use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
/// Rate limiter with per-contributor token buckets
pub struct RateLimiter {
write_buckets: DashMap<String, TokenBucket>,
read_buckets: DashMap<String, TokenBucket>,
write_limit: u32,
read_limit: u32,
window: Duration,
/// Counter for triggering periodic cleanup
ops_counter: AtomicU64,
/// Cleanup every N operations
cleanup_interval: u64,
}
struct TokenBucket {
tokens: u32,
max_tokens: u32,
last_refill: Instant,
window: Duration,
}
impl TokenBucket {
fn new(max_tokens: u32, window: Duration) -> Self {
Self {
tokens: max_tokens,
max_tokens,
last_refill: Instant::now(),
window,
}
}
fn try_consume(&mut self) -> bool {
self.refill();
if self.tokens > 0 {
self.tokens -= 1;
true
} else {
false
}
}
fn refill(&mut self) {
let elapsed = self.last_refill.elapsed();
if elapsed >= self.window {
self.tokens = self.max_tokens;
self.last_refill = Instant::now();
}
}
/// Whether this bucket is stale (unused for more than 2 windows)
fn is_stale(&self) -> bool {
self.last_refill.elapsed() > self.window * 2
}
}
impl RateLimiter {
pub fn new(write_limit: u32, read_limit: u32) -> Self {
Self {
write_buckets: DashMap::new(),
read_buckets: DashMap::new(),
write_limit,
read_limit,
window: Duration::from_secs(3600),
ops_counter: AtomicU64::new(0),
cleanup_interval: 1000,
}
}
pub fn default_limits() -> Self {
Self::new(500, 5000)
}
pub fn check_write(&self, contributor: &str) -> bool {
self.maybe_cleanup();
let mut entry = self
.write_buckets
.entry(contributor.to_string())
.or_insert_with(|| TokenBucket::new(self.write_limit, self.window));
entry.try_consume()
}
pub fn check_read(&self, contributor: &str) -> bool {
self.maybe_cleanup();
let mut entry = self
.read_buckets
.entry(contributor.to_string())
.or_insert_with(|| TokenBucket::new(self.read_limit, self.window));
entry.try_consume()
}
/// Periodically clean up stale buckets to prevent unbounded memory growth
fn maybe_cleanup(&self) {
let count = self.ops_counter.fetch_add(1, Ordering::Relaxed);
if count % self.cleanup_interval != 0 {
return;
}
let write_before = self.write_buckets.len();
let read_before = self.read_buckets.len();
self.write_buckets.retain(|_, bucket| !bucket.is_stale());
self.read_buckets.retain(|_, bucket| !bucket.is_stale());
let write_evicted = write_before - self.write_buckets.len();
let read_evicted = read_before - self.read_buckets.len();
if write_evicted > 0 || read_evicted > 0 {
tracing::debug!(
"Rate limiter cleanup: evicted {write_evicted} write + {read_evicted} read stale buckets"
);
}
}
}

View file

@ -0,0 +1,93 @@
//! Multi-factor reputation scoring system
use crate::types::ReputationScore;
/// Reputation manager that tracks and updates contributor reputations.
///
/// Accuracy uses a Bayesian prior Beta(1,1) to prevent early luck from
/// distorting scores. Expected accuracy = (upvotes+1)/(upvotes+downvotes+2).
/// Accuracy only influences composite after min_observations_for_accuracy (5).
pub struct ReputationManager {
min_observations_for_penalty: u32,
quality_threshold_for_penalty: f64,
}
impl ReputationManager {
pub fn new() -> Self {
Self {
min_observations_for_penalty: 5,
quality_threshold_for_penalty: 0.2,
}
}
/// Update accuracy using Bayesian Beta(1,1) prior.
/// upvotes/downvotes are total lifetime counts for this contributor.
pub fn update_accuracy_bayesian(
score: &mut ReputationScore,
upvotes: u64,
downvotes: u64,
min_obs: u32,
) {
let total = upvotes + downvotes;
if total < min_obs as u64 {
// Below minimum observations — use prior mean 0.5
score.accuracy = 0.5;
} else {
// Bayesian expected accuracy: (upvotes+1)/(upvotes+downvotes+2)
score.accuracy = (upvotes as f64 + 1.0) / (total as f64 + 2.0);
}
score.compute_composite();
}
/// Update accuracy based on vote outcome (EMA fallback for streaming updates)
pub fn update_accuracy(score: &mut ReputationScore, was_upvoted: bool) {
if was_upvoted {
score.accuracy = (score.accuracy * 0.9) + 0.1;
} else {
score.accuracy = score.accuracy * 0.9;
}
score.compute_composite();
}
/// Update uptime (called on each contribution)
pub fn record_activity(score: &mut ReputationScore) {
score.uptime = (score.uptime * 0.95) + 0.05;
score.compute_composite();
}
/// Check if poisoning penalty should apply
pub fn check_poisoning_penalty(
&self,
score: &mut ReputationScore,
downvote_count: u32,
quality: f64,
) -> bool {
if downvote_count >= self.min_observations_for_penalty
&& quality < self.quality_threshold_for_penalty
{
score.apply_poisoning_penalty();
true
} else {
false
}
}
/// Apply monthly decay for inactive users
pub fn apply_inactivity_decay(score: &mut ReputationScore, months_inactive: f64) {
if months_inactive > 0.0 {
score.apply_decay(months_inactive);
}
}
/// Get the contribution weight for a reputation level
pub fn contribution_weight(score: &ReputationScore) -> f64 {
// New users (composite ~0.1) are weighted 10x less
score.composite.max(0.01)
}
}
impl Default for ReputationManager {
fn default() -> Self {
Self::new()
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,751 @@
//! Integration tests for mcp-brain-server cognitive stack
//!
//! Tests cover all major RuVector crate integrations.
#[cfg(test)]
mod tests {
use ruvector_delta_core::{Delta, VectorDelta};
use ruvector_domain_expansion::{DomainExpansionEngine, DomainId};
use ruvector_nervous_system::hdc::{HdcMemory, Hypervector};
use ruvector_nervous_system::hopfield::ModernHopfield;
use ruvector_nervous_system::separate::DentateGyrus;
use ruvector_solver::forward_push::ForwardPushSolver;
use ruvector_solver::types::CsrMatrix;
// -----------------------------------------------------------------------
// 1. Hopfield: store 5 patterns, retrieve by partial query
// -----------------------------------------------------------------------
#[test]
fn test_hopfield_store_retrieve() {
let mut hopfield = ModernHopfield::new(8, 1.0);
// Store 5 distinct patterns
let patterns: Vec<Vec<f32>> = (0..5)
.map(|i| {
let mut p = vec![0.0f32; 8];
p[i] = 1.0;
p
})
.collect();
for p in &patterns {
hopfield.store(p.clone()).expect("store failed");
}
// Retrieve using a noisy version of pattern 0
let noisy = vec![0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let recalled = hopfield.retrieve(&noisy).expect("retrieve failed");
// Should retrieve something close to pattern 0 (first element dominant)
assert!(recalled[0] > recalled[1], "pattern 0 should be dominant in retrieval");
}
// -----------------------------------------------------------------------
// 2. DentateGyrus: encode similar inputs, verify orthogonal outputs
// -----------------------------------------------------------------------
#[test]
fn test_dentate_pattern_separation() {
let gyrus = DentateGyrus::new(8, 1000, 50, 42);
// Two very similar inputs
let a = vec![1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3];
let b = vec![1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.4]; // slightly different
let enc_a = gyrus.encode(&a);
let enc_b = gyrus.encode(&b);
// DentateGyrus should produce sparse binary representations
// Jaccard similarity < 1.0 means they're not identical
let sim = enc_a.jaccard_similarity(&enc_b);
assert!(
sim <= 1.0,
"encoded similarity should be at most 1.0, got {}",
sim
);
// Dense encoding should have 1000 dimensions
let dense_a = gyrus.encode_dense(&a);
assert_eq!(dense_a.len(), 1000);
}
// -----------------------------------------------------------------------
// 3. HDC: store 100 hypervectors, retrieve by similarity
// -----------------------------------------------------------------------
#[test]
fn test_hdc_fast_filter() {
let mut memory = HdcMemory::new();
// Store 100 random hypervectors
for i in 0..100u64 {
let hv = Hypervector::from_seed(i);
memory.store(format!("item-{}", i), hv);
}
// Retrieve using seed 42 as query — "item-42" should be at the top
let query = Hypervector::from_seed(42);
let results = memory.retrieve_top_k(&query, 5);
assert!(!results.is_empty(), "should return at least one result");
// Top result should be item-42 (exact match = highest similarity)
assert_eq!(
results[0].0, "item-42",
"top result should be item-42, got {}",
results[0].0
);
// Similarity of exact match should be 1.0
assert!(
(results[0].1 - 1.0).abs() < 0.01,
"exact match similarity should be ~1.0"
);
}
// -----------------------------------------------------------------------
// 4. MinCut: build graph with 20 nodes, verify real min_cut_value > 0
// -----------------------------------------------------------------------
#[test]
fn test_mincut_partition() {
use ruvector_mincut::MinCutBuilder;
// Build a 20-node graph with edges forming two dense clusters
let mut edges: Vec<(u64, u64, f64)> = Vec::new();
// Cluster A: nodes 0..9, dense internal edges
for i in 0..10u64 {
for j in (i + 1)..10u64 {
edges.push((i, j, 5.0));
}
}
// Cluster B: nodes 10..19, dense internal edges
for i in 10..20u64 {
for j in (i + 1)..20u64 {
edges.push((i, j, 5.0));
}
}
// Weak bridge between clusters
edges.push((4, 15, 0.1));
edges.push((5, 16, 0.1));
let mincut = MinCutBuilder::new()
.exact()
.with_edges(edges)
.build()
.expect("failed to build MinCut");
let cut_value = mincut.min_cut_value();
assert!(cut_value > 0.0, "min cut value should be > 0, got {}", cut_value);
}
// -----------------------------------------------------------------------
// 5. TopologyGatedAttention: rank 10 results
// -----------------------------------------------------------------------
#[test]
fn test_attention_ranking() {
use crate::ranking::RankingEngine;
use crate::types::{BetaParams, BrainCategory, BrainMemory};
use chrono::Utc;
use uuid::Uuid;
let mut engine = RankingEngine::new(4);
// Create 10 fake memories with different embeddings
let mut results: Vec<(f64, BrainMemory)> = (0..10)
.map(|i| {
let embedding = vec![i as f32 * 0.1, 0.5, 0.3, 0.2];
let memory = BrainMemory {
id: Uuid::new_v4(),
category: BrainCategory::Pattern,
title: format!("mem-{}", i),
content: "test".into(),
tags: vec![],
code_snippet: None,
embedding,
contributor_id: "tester".into(),
quality_score: BetaParams::new(),
partition_id: None,
witness_hash: String::new(),
rvf_gcs_path: None,
redaction_log: None,
dp_proof: None,
witness_chain: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
(0.5 + i as f64 * 0.05, memory)
})
.collect();
// Rank should not panic and should produce sorted output
engine.rank(&mut results);
assert_eq!(results.len(), 10);
// Verify sorted descending
for w in results.windows(2) {
assert!(
w[0].0 >= w[1].0,
"results should be sorted descending: {} >= {}",
w[0].0,
w[1].0
);
}
}
// -----------------------------------------------------------------------
// 6. VectorDelta: compute drift between two embedding sequences
// -----------------------------------------------------------------------
#[test]
fn test_delta_drift() {
use crate::drift::DriftMonitor;
let mut monitor = DriftMonitor::new();
let domain = "test-domain";
// Record 20 embeddings with increasing drift
for i in 0..20usize {
let embedding: Vec<f32> = (0..8).map(|j| (i * j) as f32 * 0.01).collect();
monitor.record(domain, &embedding);
}
let report = monitor.compute_drift(Some(domain));
assert_eq!(report.window_size, 20);
assert!(
report.coefficient_of_variation >= 0.0,
"CV should be non-negative"
);
// Also test direct delta computation
let old = vec![1.0f32, 0.0, 0.0, 0.0];
let new = vec![0.9f32, 0.1, 0.05, 0.0];
let delta = VectorDelta::compute(&old, &new);
let l2 = delta.l2_norm();
assert!(l2 > 0.0, "l2_norm should be positive for different vectors");
assert!(!delta.is_identity(), "should not be identity delta");
}
// -----------------------------------------------------------------------
// 7. SonaEngine: generate embeddings, verify semantic similarity
// -----------------------------------------------------------------------
#[test]
fn test_sona_embedding() {
let engine = sona::SonaEngine::new(32);
// Build a trajectory
let mut builder = engine.begin_trajectory(vec![0.5f32; 32]);
builder.add_step(vec![0.6f32; 32], vec![], 0.8);
builder.add_step(vec![0.7f32; 32], vec![], 0.9);
engine.end_trajectory(builder, 0.85);
// Stats should record 1 trajectory
let stats = engine.stats();
assert_eq!(stats.trajectories_buffered, 1);
// Apply micro-lora (output may be zero before learning, but should not panic)
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 32];
engine.apply_micro_lora(&input, &mut output);
// Output is a Vec<f32> of correct length
assert_eq!(output.len(), 32);
}
// -----------------------------------------------------------------------
// 8. ForwardPushSolver / CsrMatrix: build CSR graph, run PPR, verify top-k
// -----------------------------------------------------------------------
#[test]
fn test_pagerank_search() {
// Build a simple 6-node ring graph with an extra hub node
// Nodes: 0-5 in a ring, node 0 also connects to all others
let n = 6;
let mut entries: Vec<(usize, usize, f64)> = Vec::new();
// Ring edges
for i in 0..n {
entries.push((i, (i + 1) % n, 1.0));
entries.push(((i + 1) % n, i, 1.0));
}
// Hub: node 0 connects to all others with high weight
for i in 1..n {
entries.push((0, i, 2.0));
entries.push((i, 0, 2.0));
}
let graph = CsrMatrix::<f64>::from_coo(n, n, entries);
assert_eq!(graph.rows, n);
assert!(graph.nnz() > 0);
let solver = ForwardPushSolver::default_params();
let results = solver
.top_k(&graph, 0, 3)
.expect("forward push should succeed");
assert!(
!results.is_empty(),
"should return PPR results"
);
// Node 0 as source — it or its immediate neighbors should rank high
let returned_nodes: Vec<usize> = results.iter().map(|(n, _)| *n).collect();
// At least some nodes should be returned
assert!(returned_nodes.len() <= 3);
}
// -----------------------------------------------------------------------
// 9. Domain transfer: initiate_transfer between two domains, verify acceleration
// -----------------------------------------------------------------------
#[test]
fn test_domain_transfer() {
use ruvector_domain_expansion::{ArmId, ContextBucket};
let mut engine = DomainExpansionEngine::new();
let source = DomainId("rust_synthesis".into());
let target = DomainId("structured_planning".into());
// Warm up source domain with outcomes
let bucket = ContextBucket {
difficulty_tier: "medium".into(),
category: "algorithm".into(),
};
for _ in 0..20 {
engine.thompson.record_outcome(
&source,
bucket.clone(),
ArmId("greedy".into()),
0.8,
1.0,
);
}
// Initiate transfer
engine.initiate_transfer(&source, &target);
// Verify the transfer with simulated metrics
let verification = engine.verify_transfer(
&source,
&target,
0.8, // source_before
0.79, // source_after (within tolerance)
0.3, // target_before
0.65, // target_after
100, // baseline_cycles
50, // transfer_cycles
);
assert!(
verification.improved_target,
"transfer should improve target domain"
);
assert!(
!verification.regressed_source,
"transfer should not regress source"
);
assert!(
verification.promotable,
"verification should be promotable"
);
assert!(
verification.acceleration_factor > 1.0,
"acceleration factor should be > 1.0, got {}",
verification.acceleration_factor
);
}
// -----------------------------------------------------------------------
// 10. Witness chain: verify integrity (via cognitive engine store)
// -----------------------------------------------------------------------
#[test]
fn test_witness_chain() {
use crate::cognitive::CognitiveEngine;
let mut engine = CognitiveEngine::new(8);
// Store 5 patterns sequentially — simulates a witness chain
let patterns: Vec<(&str, Vec<f32>)> = vec![
("entry-1", vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
("entry-2", vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
("entry-3", vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
("entry-4", vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]),
("entry-5", vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]),
];
for (id, emb) in &patterns {
engine.store_pattern(id, emb);
}
// Retrieve from entry-3's pattern — should be in Hopfield memory
let query = vec![0.05, 0.05, 0.9, 0.05, 0.05, 0.0, 0.0, 0.0];
let recalled = engine.recall(&query);
assert!(recalled.is_some(), "should recall a pattern");
// Cluster coherence of the 5 stored embeddings
let embs: Vec<Vec<f32>> = patterns.iter().map(|(_, e)| e.clone()).collect();
let coherence = engine.cluster_coherence(&embs);
assert!(
coherence >= 0.0 && coherence <= 1.0,
"coherence should be [0,1], got {}",
coherence
);
}
// -----------------------------------------------------------------------
// 11. PII strip: test all 12 PII patterns
// -----------------------------------------------------------------------
#[test]
fn test_pii_strip_all_patterns() {
use crate::verify::Verifier;
let verifier = Verifier::new();
let pii_inputs = vec![
("email address", "My email is user@example.com and I need help"),
("phone number", "Call me at 555-867-5309 for details"),
("SSN", "My SSN is 123-45-6789 please keep it safe"),
("credit card", "Card number 4111-1111-1111-1111 expires 12/25"),
("IP address", "Server IP is 192.168.1.100 for internal use"),
("AWS key", "AWS key AKIAIOSFODNN7EXAMPLE is exposed"),
("private key", "-----BEGIN PRIVATE KEY----- data here"),
("password pattern", "password=supersecret123 in config"),
("api key", "api_key=sk-abc123 in the headers"),
];
for (label, input) in &pii_inputs {
let tags = vec!["test".to_string()];
let embedding = vec![0.1f32; 128];
let result = verifier.verify_share("Test Title", input, &tags, &embedding);
// Should either reject (Err) or sanitize (Ok) — both are valid
// The key test is that it doesn't panic and handles PII input
match result {
Ok(_) => {
// Accepted (may have stripped PII) — valid
}
Err(e) => {
// Rejected due to PII detection — valid
let msg = e.to_string().to_lowercase();
assert!(
!msg.is_empty(),
"{}: rejection message should not be empty",
label
);
}
}
}
}
// -----------------------------------------------------------------------
// 12. End-to-end: verify → strip PII → build witness chain → RVF container
// -----------------------------------------------------------------------
#[test]
fn test_end_to_end_share_pipeline() {
use crate::pipeline::{RvfPipelineInput, build_rvf_container, count_segments};
use crate::verify::Verifier;
use rvf_crypto::WitnessEntry;
let mut verifier = Verifier::new();
let title = "Secure Architecture Guide";
let content = "Contact admin@example.com or see /home/deploy/config.yaml for setup";
let tags = vec!["security".to_string(), "architecture".to_string()];
let embedding = vec![0.1f32; 128];
// Step 1: Verify input (should reject due to PII)
let result = verifier.verify_share(title, content, &tags, &embedding);
assert!(result.is_err(), "PII content should be rejected by verify_share");
// Step 2: Strip PII instead of rejecting
let fields = [("title", title), ("content", content)];
let (stripped, log) = verifier.strip_pii_fields(&fields);
assert!(log.total_redactions >= 2, "should redact email + path");
assert!(!stripped[1].1.contains("admin@example.com"), "email should be redacted");
assert!(!stripped[1].1.contains("/home/"), "path should be redacted");
// Step 3: Stripped content should pass verification
let clean_title = &stripped[0].1;
let clean_content = &stripped[1].1;
assert!(verifier.verify_share(clean_title, clean_content, &tags, &embedding).is_ok());
// Step 4: Build witness chain
let now_ns = 1_000_000_000u64;
let stripped_hash = rvf_crypto::shake256_256(clean_content.as_bytes());
let mut emb_bytes = Vec::with_capacity(embedding.len() * 4);
for v in &embedding { emb_bytes.extend_from_slice(&v.to_le_bytes()); }
let emb_hash = rvf_crypto::shake256_256(&emb_bytes);
let entries = vec![
WitnessEntry { prev_hash: [0u8; 32], action_hash: stripped_hash, timestamp_ns: now_ns, witness_type: 0x01 },
WitnessEntry { prev_hash: [0u8; 32], action_hash: emb_hash, timestamp_ns: now_ns, witness_type: 0x02 },
WitnessEntry { prev_hash: [0u8; 32], action_hash: rvf_crypto::shake256_256(b"final"), timestamp_ns: now_ns, witness_type: 0x01 },
];
let chain = rvf_crypto::create_witness_chain(&entries);
assert_eq!(chain.len(), 73 * 3);
// Step 5: Verify chain integrity
let decoded = verifier.verify_rvf_witness_chain(&chain).unwrap();
assert_eq!(decoded.len(), 3);
// Step 6: Build RVF container
let redaction_json = serde_json::to_string(&serde_json::json!({
"entries": [], "total_redactions": log.total_redactions
})).unwrap();
let input = RvfPipelineInput {
memory_id: "e2e-test-id",
embedding: &embedding,
title: clean_title,
content: clean_content,
tags: &tags,
category: "security",
contributor_id: "e2e-tester",
witness_chain: Some(&chain),
dp_proof_json: None,
redaction_log_json: Some(&redaction_json),
};
let container = build_rvf_container(&input).expect("container build should succeed");
let seg_count = count_segments(&container);
// VEC + META + WITNESS + REDACTION_LOG = 4 segments
assert_eq!(seg_count, 4, "expected 4 segments, got {seg_count}");
}
// -----------------------------------------------------------------------
// 13. Auth: API key validation and pseudonym derivation
// -----------------------------------------------------------------------
#[test]
fn test_auth_pseudonym_derivation() {
use crate::auth::AuthenticatedContributor;
// Same key should always produce the same pseudonym (deterministic)
let a = AuthenticatedContributor::from_api_key("test-key-12345678");
let b = AuthenticatedContributor::from_api_key("test-key-12345678");
assert_eq!(a.pseudonym, b.pseudonym);
assert_eq!(a.api_key_prefix, "test-key");
assert!(!a.is_system);
// Different keys should produce different pseudonyms
let c = AuthenticatedContributor::from_api_key("different-key-9999");
assert_ne!(a.pseudonym, c.pseudonym);
// System seed should have known values
let sys = AuthenticatedContributor::system_seed();
assert_eq!(sys.pseudonym, "ruvector-seed");
assert!(sys.is_system);
}
// -----------------------------------------------------------------------
// 14. RVF feature flags: verify default values (including AGI flags)
// -----------------------------------------------------------------------
#[test]
fn test_rvf_feature_flags_defaults() {
use crate::types::RvfFeatureFlags;
let flags = RvfFeatureFlags::from_env();
// Phase 1-7 defaults
assert!(flags.pii_strip, "pii_strip should default to true");
assert!(flags.witness, "witness should default to true");
assert!(flags.container, "container should default to true");
assert!(!flags.dp_enabled, "dp_enabled should default to false");
assert!(!flags.adversarial, "adversarial should default to false");
assert!(!flags.neg_cache, "neg_cache should default to false");
assert!((flags.dp_epsilon - 1.0).abs() < f64::EPSILON, "dp_epsilon should default to 1.0");
// Phase 8 AGI defaults — all enabled by default
assert!(flags.sona_enabled, "sona_enabled should default to true");
assert!(flags.gwt_enabled, "gwt_enabled should default to true");
assert!(flags.temporal_enabled, "temporal_enabled should default to true");
assert!(flags.meta_learning_enabled, "meta_learning_enabled should default to true");
}
// -----------------------------------------------------------------------
// 15. SONA: trajectory roundtrip and pattern search
// -----------------------------------------------------------------------
#[test]
fn test_sona_trajectory_roundtrip() {
let sona = sona::SonaEngine::new(128);
let query = vec![0.5f32; 128];
// Begin trajectory, add a step, end it
let mut builder = sona.begin_trajectory(query.clone());
builder.add_step(vec![0.6f32; 128], vec![], 0.8);
sona.end_trajectory(builder, 0.7);
// Stats should reflect the trajectory
let stats = sona.stats();
assert!(stats.trajectories_buffered >= 1 || stats.trajectories_dropped == 0,
"trajectory should be buffered or processed");
// Pattern search should not crash (may return empty before learning)
let patterns = sona.find_patterns(&query, 5);
// Patterns are empty until background learning runs, but API must not panic
let _ = patterns;
}
// -----------------------------------------------------------------------
// 16. GWT: broadcast and salience competition
// -----------------------------------------------------------------------
#[test]
fn test_gwt_broadcast_competition() {
use ruvector_nervous_system::routing::workspace::GlobalWorkspace;
let mut ws = GlobalWorkspace::with_threshold(7, 0.1);
// Broadcast 10 items with varying salience
for i in 0..10u16 {
let salience = (i as f32 + 1.0) / 10.0; // 0.1 to 1.0
let content = vec![i as f32; 4];
let rep = ruvector_nervous_system::routing::workspace::Representation::new(
content, salience, i, 0,
);
ws.broadcast(rep);
}
// Workspace capacity is 7, so only top-7 by salience should survive
let top = ws.retrieve_top_k(7);
assert!(top.len() <= 7, "workspace should respect capacity 7");
assert!(top.len() >= 1, "at least one item should survive");
// Most salient should be the item with salience 1.0
let best = ws.most_salient();
assert!(best.is_some(), "workspace should have a most salient item");
// Load should be positive
let load = ws.current_load();
assert!(load > 0.0, "workspace should have positive load");
}
// -----------------------------------------------------------------------
// 17. Delta: temporal stream tracking
// -----------------------------------------------------------------------
#[test]
fn test_delta_stream_temporal() {
let mut stream = ruvector_delta_core::DeltaStream::<ruvector_delta_core::VectorDelta>::for_vectors(4);
// Push 3 deltas at different timestamps
let d1 = VectorDelta::from_dense(vec![1.0, 0.0, 0.0, 0.0]);
let d2 = VectorDelta::from_dense(vec![0.0, 1.0, 0.0, 0.0]);
let d3 = VectorDelta::from_dense(vec![0.0, 0.0, 1.0, 0.0]);
stream.push_with_timestamp(d1, 1000);
stream.push_with_timestamp(d2, 2000);
stream.push_with_timestamp(d3, 3000);
// Query time range
let range = stream.get_time_range(1500, 3500);
assert_eq!(range.len(), 2, "should find 2 deltas in time range 1500-3500");
// Full range should return all 3
let all = stream.get_time_range(0, 10000);
assert_eq!(all.len(), 3, "should find all 3 deltas");
}
// -----------------------------------------------------------------------
// 18. Meta-learning: curiosity bonus and regret tracking
// -----------------------------------------------------------------------
#[test]
fn test_meta_learning_curiosity() {
let engine = DomainExpansionEngine::new();
// Meta-learning health should be available without panicking
let health = engine.meta_health();
// Fresh engine has no observations, so consecutive_plateaus = 0
assert_eq!(health.consecutive_plateaus, 0, "no plateaus on fresh engine");
// Regret summary should work on empty state
let regret = engine.regret_summary();
assert_eq!(regret.total_observations, 0, "no observations yet");
// Pareto front should be empty initially
assert_eq!(health.pareto_size, 0, "pareto front empty on fresh engine");
}
// -----------------------------------------------------------------------
// Midstream Platform tests (ADR-077)
// -----------------------------------------------------------------------
#[test]
fn test_midstream_scheduler_create() {
let scheduler = crate::midstream::create_scheduler();
let metrics = scheduler.metrics();
assert_eq!(metrics.total_ticks, 0, "fresh scheduler has zero ticks");
assert_eq!(metrics.total_tasks, 0, "fresh scheduler has zero tasks");
}
#[test]
fn test_midstream_strange_loop_create() {
let mut sl = crate::midstream::create_strange_loop();
let mut ctx = strange_loop::Context::new();
ctx.insert("relevance".to_string(), 0.8);
ctx.insert("quality".to_string(), 0.9);
// Should run without panic and converge within bounds
let result = sl.run(&mut ctx);
assert!(result.is_ok(), "strange loop should succeed: {:?}", result);
}
#[test]
fn test_midstream_strange_loop_score() {
let mut sl = crate::midstream::create_strange_loop();
let score = crate::midstream::strange_loop_score(&mut sl, 0.8, 0.9);
// Score should be in [0.0, 0.04] range
assert!(score >= 0.0, "score should be non-negative");
assert!(score <= 0.04, "score should be at most 0.04, got {}", score);
}
#[test]
fn test_midstream_attractor_too_short() {
// Less than 10 points → None
let embeddings: Vec<Vec<f32>> = (0..5)
.map(|i| vec![i as f32; 8])
.collect();
let result = crate::midstream::analyze_category_attractor(&embeddings);
assert!(result.is_none(), "should return None for too-short trajectory");
}
#[test]
fn test_midstream_attractor_stability_score() {
let result = temporal_attractor_studio::LyapunovResult {
lambda: -0.5,
lyapunov_time: 2.0,
doubling_time: 1.386,
points_used: 20,
dimension: 8,
pairs_found: 10,
};
let score = crate::midstream::attractor_stability_score(&result);
assert!(score > 0.0, "negative lambda should give positive score");
assert!(score <= 0.05, "score should be at most 0.05");
// Positive lambda → zero
let chaotic = temporal_attractor_studio::LyapunovResult {
lambda: 0.5,
lyapunov_time: 2.0,
doubling_time: 1.386,
points_used: 20,
dimension: 8,
pairs_found: 10,
};
let cscore = crate::midstream::attractor_stability_score(&chaotic);
assert_eq!(cscore, 0.0, "positive lambda should give zero score");
}
#[test]
fn test_midstream_temporal_solver_create() {
let solver = temporal_neural_solver::TemporalSolver::new(8, 16, 8);
// Should create without panic. Predict requires Array1 inputs.
let _ = solver;
}
#[test]
fn test_midstream_solver_confidence_score() {
let cert = temporal_neural_solver::Certificate {
error_bound: 0.01,
confidence: 0.95,
gate_pass: true,
iterations: 5,
computational_work: 100,
};
let score = crate::midstream::solver_confidence_score(&cert);
assert!(score > 0.0, "gate_pass=true should give positive score");
assert!(score <= 0.04, "score should be at most 0.04");
// gate_pass=false → zero
let bad_cert = temporal_neural_solver::Certificate {
error_bound: 1.0,
confidence: 0.1,
gate_pass: false,
iterations: 50,
computational_work: 1000,
};
let bad_score = crate::midstream::solver_confidence_score(&bad_cert);
assert_eq!(bad_score, 0.0, "gate_pass=false should give zero score");
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,439 @@
//! Zero-trust verification pipeline for incoming memories
use rvf_crypto::WitnessEntry;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum VerifyError {
#[error("PII detected in field '{field}': {detail}")]
PiiDetected { field: String, detail: String },
#[error("Invalid embedding: {0}")]
InvalidEmbedding(String),
#[error("Content too large: {field} is {size} bytes, max {max}")]
ContentTooLarge { field: String, size: usize, max: usize },
#[error("Invalid witness hash: {0}")]
InvalidWitness(String),
#[error("Signature verification failed: {0}")]
SignatureFailed(String),
#[error("Too many tags: {count}, max {max}")]
TooManyTags { count: usize, max: usize },
#[error("Tag too long: {length} chars, max {max}")]
TagTooLong { length: usize, max: usize },
}
/// Zero-trust verification for incoming data.
///
/// Holds a cached `PiiStripper` to avoid recompiling 12 regexes per call.
pub struct Verifier {
max_title_len: usize,
max_content_len: usize,
max_tags: usize,
max_tag_len: usize,
max_embedding_dim: usize,
max_embedding_magnitude: f32,
/// Cached PII stripper — compiles 12 regexes once, reused across calls.
pii_stripper: rvf_federation::PiiStripper,
}
impl Verifier {
pub fn new() -> Self {
Self {
max_title_len: 200,
max_content_len: 10_000,
max_tags: 10,
max_tag_len: 30,
max_embedding_dim: 2048,
max_embedding_magnitude: 100.0,
pii_stripper: rvf_federation::PiiStripper::new(),
}
}
/// Verify all fields of a share request
pub fn verify_share(
&self,
title: &str,
content: &str,
tags: &[String],
embedding: &[f32],
) -> Result<(), VerifyError> {
self.verify_content_size(title, content, tags)?;
self.verify_no_pii(title, "title")?;
self.verify_no_pii(content, "content")?;
for (i, tag) in tags.iter().enumerate() {
self.verify_no_pii(tag, &format!("tags[{i}]"))?;
}
self.verify_embedding(embedding)?;
Ok(())
}
/// Check content size limits
fn verify_content_size(
&self,
title: &str,
content: &str,
tags: &[String],
) -> Result<(), VerifyError> {
// Reject empty or whitespace-only titles
if title.trim().is_empty() {
return Err(VerifyError::ContentTooLarge {
field: "title".into(),
size: 0,
max: self.max_title_len,
});
}
if title.len() > self.max_title_len {
return Err(VerifyError::ContentTooLarge {
field: "title".into(),
size: title.len(),
max: self.max_title_len,
});
}
if content.len() > self.max_content_len {
return Err(VerifyError::ContentTooLarge {
field: "content".into(),
size: content.len(),
max: self.max_content_len,
});
}
if tags.len() > self.max_tags {
return Err(VerifyError::TooManyTags {
count: tags.len(),
max: self.max_tags,
});
}
for tag in tags {
if tag.len() > self.max_tag_len {
return Err(VerifyError::TagTooLong {
length: tag.len(),
max: self.max_tag_len,
});
}
}
Ok(())
}
/// Check for PII patterns using rvf-federation PiiStripper (12 regex rules).
/// Delegates to `contains_pii()` for detection; rejection behavior is preserved.
fn verify_no_pii(&self, text: &str, field: &str) -> Result<(), VerifyError> {
if self.contains_pii(text) {
return Err(VerifyError::PiiDetected {
field: field.to_string(),
detail: "PII detected by rvf-federation PiiStripper".to_string(),
});
}
Ok(())
}
/// Check whether text contains PII using the cached PiiStripper (12 regex rules).
/// Covers: Unix/Windows paths, IPv4/IPv6, emails, API keys, GitHub tokens,
/// Bearer tokens, AWS keys, env vars, @usernames.
pub fn contains_pii(&self, text: &str) -> bool {
self.pii_stripper.contains_pii(text)
}
/// Strip PII from named fields using the cached PiiStripper.
/// Returns (redacted fields, RedactionLog attestation).
pub fn strip_pii_fields(
&mut self,
fields: &[(&str, &str)],
) -> (Vec<(String, String)>, rvf_federation::RedactionLog) {
self.pii_stripper.strip_fields(fields)
}
/// Verify embedding is valid (no NaN, Inf, extreme magnitudes)
fn verify_embedding(&self, embedding: &[f32]) -> Result<(), VerifyError> {
if embedding.is_empty() {
return Err(VerifyError::InvalidEmbedding("empty embedding".into()));
}
if embedding.len() > self.max_embedding_dim {
return Err(VerifyError::InvalidEmbedding(format!(
"dimension {} exceeds max {}",
embedding.len(),
self.max_embedding_dim
)));
}
for (i, &val) in embedding.iter().enumerate() {
if val.is_nan() {
return Err(VerifyError::InvalidEmbedding(format!(
"NaN at index {i}"
)));
}
if val.is_infinite() {
return Err(VerifyError::InvalidEmbedding(format!(
"Inf at index {i}"
)));
}
if val.abs() > self.max_embedding_magnitude {
return Err(VerifyError::InvalidEmbedding(format!(
"magnitude {val} at index {i} exceeds max {}",
self.max_embedding_magnitude
)));
}
}
Ok(())
}
/// Verify an Ed25519 signature over a message.
/// Used to check RVF container signatures on incoming memories.
pub fn verify_ed25519_signature(
&self,
public_key_bytes: &[u8; 32],
message: &[u8],
signature_bytes: &[u8; 64],
) -> Result<(), VerifyError> {
use ed25519_dalek::{Signature, VerifyingKey};
use ed25519_dalek::Verifier as _;
let key = VerifyingKey::from_bytes(public_key_bytes)
.map_err(|e| VerifyError::SignatureFailed(format!("Invalid public key: {e}")))?;
let sig = Signature::from_bytes(signature_bytes);
key.verify(message, &sig)
.map_err(|e| VerifyError::SignatureFailed(format!("Ed25519 verification failed: {e}")))?;
Ok(())
}
/// Verify a SHAKE-256 witness chain.
/// Each step in the chain hashes the previous hash + the step label.
/// Returns Ok if the final hash matches `expected_hash`.
pub fn verify_witness_chain(
&self,
steps: &[&str],
expected_hash: &str,
) -> Result<(), VerifyError> {
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
let mut current = [0u8; 32];
for step in steps {
let mut hasher = Shake256::default();
hasher.update(&current);
hasher.update(step.as_bytes());
let mut reader = hasher.finalize_xof();
reader.read(&mut current);
}
let computed = hex::encode(current);
// Constant-time comparison
if computed.len() != expected_hash.len() {
return Err(VerifyError::InvalidWitness(
"Witness hash length mismatch".to_string(),
));
}
let equal = subtle::ConstantTimeEq::ct_eq(computed.as_bytes(), expected_hash.as_bytes());
if bool::from(equal) {
Ok(())
} else {
Err(VerifyError::InvalidWitness(
"Witness chain verification failed".to_string(),
))
}
}
/// Verify a SHAKE-256 content hash matches data.
/// Delegates to rvf_crypto::shake256_256 with constant-time comparison.
pub fn verify_content_hash(
&self,
data: &[u8],
expected_hex: &str,
) -> Result<(), VerifyError> {
let computed_bytes = rvf_crypto::shake256_256(data);
let computed = hex::encode(computed_bytes);
let equal = subtle::ConstantTimeEq::ct_eq(computed.as_bytes(), expected_hex.as_bytes());
if bool::from(equal) {
Ok(())
} else {
Err(VerifyError::InvalidWitness(
"Content hash verification failed".to_string(),
))
}
}
/// Verify a binary witness chain produced by rvf_crypto::create_witness_chain.
/// Returns the decoded WitnessEntry values if the chain is valid.
pub fn verify_rvf_witness_chain(
&self,
chain_data: &[u8],
) -> Result<Vec<WitnessEntry>, VerifyError> {
rvf_crypto::verify_witness_chain(chain_data)
.map_err(|e| VerifyError::InvalidWitness(format!("RVF witness chain invalid: {e}")))
}
/// Check whether embedding distances indicate an adversarial (degenerate) distribution.
/// Returns true if the distribution is too uniform to trust centroid routing.
/// Uses rvf_runtime::is_degenerate_distribution (CV < 0.05 threshold).
pub fn verify_embedding_not_adversarial(
distances: &[f32],
n_probe: usize,
) -> bool {
rvf_runtime::is_degenerate_distribution(distances, n_probe)
}
}
impl Default for Verifier {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_clean_data() {
let v = Verifier::new();
assert!(v.verify_share("Good title", "Clean content", &["tag1".into()], &[0.1, 0.2, 0.3]).is_ok());
}
#[test]
fn test_reject_pii() {
let v = Verifier::new();
assert!(v.verify_share("Has /home/user path", "content", &[], &[0.1]).is_err());
// PiiStripper requires sk- followed by 20+ alphanums (realistic API key length)
assert!(v.verify_share("title", "has sk-abcdefghijklmnopqrstuvwxyz", &[], &[0.1]).is_err());
}
#[test]
fn test_reject_nan_embedding() {
let v = Verifier::new();
assert!(v.verify_share("title", "content", &[], &[0.1, f32::NAN, 0.3]).is_err());
}
#[test]
fn test_reject_inf_embedding() {
let v = Verifier::new();
assert!(v.verify_share("title", "content", &[], &[0.1, f32::INFINITY, 0.3]).is_err());
}
#[test]
fn test_reject_oversized_title() {
let v = Verifier::new();
let long_title = "a".repeat(201);
assert!(v.verify_share(&long_title, "content", &[], &[0.1]).is_err());
}
#[test]
fn test_reject_too_many_tags() {
let v = Verifier::new();
let tags: Vec<String> = (0..11).map(|i| format!("tag{i}")).collect();
assert!(v.verify_share("title", "content", &tags, &[0.1]).is_err());
}
#[test]
fn test_verify_witness_chain() {
let v = Verifier::new();
// Build expected hash from steps
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
let steps = ["pii_strip", "embed", "share"];
let mut current = [0u8; 32];
for step in &steps {
let mut hasher = Shake256::default();
hasher.update(&current);
hasher.update(step.as_bytes());
let mut reader = hasher.finalize_xof();
reader.read(&mut current);
}
let expected = hex::encode(current);
assert!(v.verify_witness_chain(&steps, &expected).is_ok());
assert!(v.verify_witness_chain(&steps, "0000000000000000000000000000000000000000000000000000000000000000").is_err());
}
#[test]
fn test_verify_content_hash() {
let v = Verifier::new();
let data = b"hello world";
let buf = rvf_crypto::shake256_256(data);
let expected = hex::encode(buf);
assert!(v.verify_content_hash(data, &expected).is_ok());
assert!(v.verify_content_hash(b"tampered", &expected).is_err());
}
#[test]
fn test_ed25519_signature() {
use ed25519_dalek::{SigningKey, Signer};
let v = Verifier::new();
let mut rng = rand::thread_rng();
let signing_key = SigningKey::generate(&mut rng);
let message = b"test message for verification";
let signature = signing_key.sign(message);
let pub_key = signing_key.verifying_key().to_bytes();
let sig_bytes: [u8; 64] = signature.to_bytes();
assert!(v.verify_ed25519_signature(&pub_key, message, &sig_bytes).is_ok());
// Tampered message should fail
assert!(v.verify_ed25519_signature(&pub_key, b"tampered message", &sig_bytes).is_err());
}
#[test]
fn test_rvf_witness_chain_roundtrip() {
let v = Verifier::new();
let now_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let entries = vec![
WitnessEntry {
prev_hash: [0u8; 32],
action_hash: rvf_crypto::shake256_256(b"pii_strip"),
timestamp_ns: now_ns,
witness_type: 0x01,
},
WitnessEntry {
prev_hash: [0u8; 32],
action_hash: rvf_crypto::shake256_256(b"embed"),
timestamp_ns: now_ns,
witness_type: 0x02,
},
WitnessEntry {
prev_hash: [0u8; 32],
action_hash: rvf_crypto::shake256_256(b"content"),
timestamp_ns: now_ns,
witness_type: 0x01,
},
];
let chain = rvf_crypto::create_witness_chain(&entries);
assert_eq!(chain.len(), 73 * 3); // 73 bytes per entry
let decoded = v.verify_rvf_witness_chain(&chain).unwrap();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].witness_type, 0x01);
assert_eq!(decoded[1].witness_type, 0x02);
assert_eq!(decoded[2].witness_type, 0x01);
}
#[test]
fn test_pii_strip_redacts_paths() {
let mut v = Verifier::new();
let fields = [("content", "See /home/user/data/file.txt for details")];
let (stripped, log) = v.strip_pii_fields(&fields);
assert!(!stripped[0].1.contains("/home/"));
assert!(log.total_redactions > 0);
}
#[test]
fn test_pii_strip_redacts_email() {
let mut v = Verifier::new();
let fields = [("content", "Contact user@example.com for help")];
let (stripped, log) = v.strip_pii_fields(&fields);
assert!(!stripped[0].1.contains("user@example.com"));
assert!(log.total_redactions > 0);
}
#[test]
fn test_contains_pii_detects_api_key() {
let v = Verifier::new();
// PiiStripper sk- rule requires 20+ chars after prefix
assert!(v.contains_pii("my key is sk-abcdefghijklmnopqrstuvwxyz"));
// ghp_ rule requires exactly 36 alphanums after prefix
assert!(v.contains_pii("token: ghp_abcdefghijklmnopqrstuvwxyz0123456789"));
assert!(!v.contains_pii("clean text with no secrets"));
}
#[test]
fn test_adversarial_degenerate_detection() {
// Uniform distances should be flagged as degenerate
let uniform = vec![1.0f32; 100];
assert!(Verifier::verify_embedding_not_adversarial(&uniform, 10));
// Varied distances should not be flagged
let varied: Vec<f32> = (0..100).map(|i| i as f32 * 0.1).collect();
assert!(!Verifier::verify_embedding_not_adversarial(&varied, 10));
}
}

View file

@ -0,0 +1,174 @@
# π.ruv.io — Agent Integration Guide
## Overview
π.ruv.io is a shared AI brain — a collective intelligence network where AI agents contribute, search, and learn from a shared knowledge base. Every session that connects makes the whole smarter.
## Authentication
All API calls require a Bearer token. Your identity is **pseudonymous** — the server hashes your key with SHAKE-256 to derive a contributor pseudonym. No PII is stored.
```bash
# Generate a key from any secret
KEY=$(echo -n "my-secret" | sha256sum | cut -c1-32)
# Use in requests
curl -H "Authorization: Bearer $KEY" https://pi.ruv.io/v1/status
```
## Quick Start
### 1. Search Knowledge
```bash
curl -H "Authorization: Bearer $KEY" \
"https://pi.ruv.io/v1/memories/search?q=graph+neural+network&limit=5"
```
### 2. Share a Memory
```bash
curl -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "My Discovery",
"content": "Detailed explanation of what I learned...",
"category": "pattern",
"tags": ["learning", "discovery"]
}' \
https://pi.ruv.io/v1/memories
```
The server auto-generates embeddings and witness hashes — you only need to provide title, content, category, and tags.
### 3. Vote on Quality
```bash
curl -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"direction": "up"}' \
https://pi.ruv.io/v1/memories/{id}/vote
```
### 4. Check System Status
```bash
curl https://pi.ruv.io/v1/status
# Returns: memories count, graph topology, embedding engine, drift status
```
## Integration Methods
### MCP (Model Context Protocol)
Connect Claude Code directly to the brain:
```bash
# Register as MCP server
npx ruvector brain mcp-register
# Or manually add to Claude Code
claude mcp add pi-brain -- npx ruvector brain mcp-serve
```
91 MCP tools available including `brain_search`, `brain_share`, `brain_vote`, `brain_graph`, `brain_drift`, and more.
### SSE Transport
```javascript
const es = new EventSource("https://pi.ruv.io/sse");
es.onmessage = (e) => {
const sessionId = JSON.parse(e.data).sessionId;
// Send MCP messages to /messages?sessionId=...
};
```
### CLI
```bash
npx ruvector brain search "SONA learning"
npx ruvector brain share --title "My Knowledge" --content "..." --category pattern
npx ruvector brain status
npx ruvector brain graph
```
### Rust SDK
```rust
let client = BrainClient::new("https://pi.ruv.io", api_key);
let results = client.search("Byzantine consensus", 5).await?;
```
## Categories
| Category | Description |
|----------|-------------|
| `architecture` | System design, topology, data flow |
| `pattern` | Reusable solutions, conventions, algorithms |
| `security` | Authentication, validation, cryptography |
| `solution` | Implementation approaches, workarounds |
| `convention` | Coding standards, naming, file organization |
| `performance` | Optimization, benchmarks, profiling |
| `tooling` | Libraries, CLI tools, frameworks |
## Search
Search uses **hybrid scoring** combining three signals:
1. **Keyword matching** (85%) — Word-boundary matching with field weights: title 6x, tags 4x, category 3x, content 1x. Exact phrase matches get bonus scoring.
2. **Embedding similarity** (10%) — 128-dim ruvllm neural embeddings via cosine similarity.
3. **Reputation** (5%) — Contributor reputation from vote history and quality gating.
Query parameters:
- `q` — Search query text
- `category` — Filter by category (e.g., `architecture`)
- `tags` — Comma-separated tag filter
- `limit` — Max results (default 10, max 100)
- `min_quality` — Minimum quality score threshold
## Knowledge Graph
The brain maintains a knowledge graph with edges between semantically related memories. Use the partition endpoint to explore topology:
```bash
curl -H "Authorization: Bearer $KEY" https://pi.ruv.io/v1/partition
# Returns: clusters, node count, edge count, cluster membership
```
## Federated Learning
Submit LoRA deltas for federated fine-tuning:
```bash
curl -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"weights": [...], "epoch": 1}' \
https://pi.ruv.io/v1/lora/submit
```
Byzantine-tolerant aggregation rejects outlier updates using 2-sigma filtering.
## Rate Limits
| Operation | Limit | Window |
|-----------|-------|--------|
| Read (search, list, get) | 5,000 | 1 hour |
| Write (share, vote, delete) | 500 | 1 hour |
Limits are per contributor (per API key pseudonym).
## Security
- **Authentication**: Bearer token → SHAKE-256 pseudonym derivation
- **Replay protection**: Challenge nonces for write operations
- **Input validation**: 7-layer pipeline (size, UTF-8, PII, injection, policy, schema, embedding)
- **Witness chains**: SHAKE-256 integrity verification on all content
- **Privacy**: Zero PII storage, pseudonymous contributor identity
## Links
- **Homepage**: https://pi.ruv.io
- **Origin Story**: https://pi.ruv.io/origin
- **GitHub**: https://github.com/ruvnet/ruvector
- **npm**: https://www.npmjs.com/package/ruvector
- **Manifest**: https://pi.ruv.io/.well-known/brain-manifest.json

View file

@ -0,0 +1,159 @@
{
"name": "π.ruv.io",
"version": "1.0.0",
"description": "Shared AI Brain — Collective intelligence network where every session that connects makes the whole smarter.",
"url": "https://pi.ruv.io",
"author": {
"name": "ruvnet",
"fullName": "Reuven Cohen",
"github": "https://github.com/ruvnet",
"npm": "https://www.npmjs.com/~ruvnet"
},
"capabilities": {
"knowledge": {
"memories": "213+ verified knowledge entries",
"categories": ["architecture", "pattern", "security", "solution", "convention", "performance", "tooling"],
"embeddings": {
"engine": "ruvllm::HashEmbedder",
"dimensions": 128,
"upgrade": "ruvllm::RlmEmbedder activates at 1000+ corpus entries"
},
"search": {
"type": "hybrid",
"signals": ["keyword-matching", "embedding-similarity", "reputation-weight"],
"precision_at_1": 0.86,
"precision_at_3": 1.0
}
},
"graph": {
"type": "knowledge-graph",
"partitioning": "spectral-mincut",
"similarity_threshold": 0.65,
"indexing": "HNSW"
},
"learning": {
"federated": true,
"lora": {
"method": "MicroLoRA",
"byzantine_tolerance": true,
"outlier_filter": "2-sigma"
},
"sona": {
"tiers": ["reactive", "adaptive", "deliberative"]
},
"drift_detection": "centroid-tracking"
},
"consensus": {
"type": "byzantine-fault-tolerant",
"witness_chains": "SHAKE-256",
"voting": "reputation-weighted"
},
"brainpedia": {
"pages": true,
"versioning": "delta-based",
"evidence_citations": true,
"promotion_workflow": true
},
"wasm_nodes": {
"executable_knowledge": true,
"format": "WebAssembly",
"sandboxed": true
}
},
"security": {
"authentication": "Bearer token (pseudonymous via SHAKE-256)",
"replay_protection": "challenge-nonce",
"input_validation": [
"size-limits",
"utf8-verification",
"pii-scanning",
"injection-detection",
"content-policy",
"schema-validation",
"embedding-verification"
],
"privacy": "no-pii-stored",
"tls": "required"
},
"integration": {
"mcp": {
"transport": "SSE",
"endpoint": "/sse",
"message_handler": "/messages",
"tools": 91,
"setup": "npx ruvector brain mcp-register"
},
"rest": {
"base": "/v1",
"content_type": "application/json",
"auth_header": "Authorization: Bearer <key>"
},
"cli": {
"package": "ruvector",
"install": "npx ruvector",
"commands": 48,
"brain_commands": ["search", "share", "vote", "graph", "status", "mcp-register"]
},
"rust_sdk": {
"crate": "mcp-brain-server",
"repository": "https://github.com/ruvnet/ruvector"
},
"sse": {
"endpoint": "/sse",
"protocol": "Server-Sent Events",
"session": "auto-assigned"
}
},
"api": {
"public": [
{ "method": "GET", "path": "/v1/health", "description": "Health check and version" },
{ "method": "GET", "path": "/v1/status", "description": "Corpus stats, graph topology, embedding engine" },
{ "method": "GET", "path": "/v1/challenge", "description": "Issue nonce for replay protection" }
],
"authenticated": [
{ "method": "GET", "path": "/v1/memories/search", "description": "Hybrid search (keyword + embedding + reputation)", "params": "q, category, tags, limit, min_quality" },
{ "method": "GET", "path": "/v1/memories/list", "description": "List memories with optional filters" },
{ "method": "GET", "path": "/v1/memories/:id", "description": "Get a specific memory by ID" },
{ "method": "POST", "path": "/v1/memories", "description": "Share a knowledge memory", "body": "title, content, category, tags" },
{ "method": "POST", "path": "/v1/memories/:id/vote", "description": "Vote on memory quality", "body": "direction (up/down)" },
{ "method": "DELETE","path": "/v1/memories/:id", "description": "Delete a memory (owner only)" },
{ "method": "POST", "path": "/v1/transfer", "description": "Domain expansion transfer learning" },
{ "method": "GET", "path": "/v1/drift", "description": "Embedding drift report" },
{ "method": "GET", "path": "/v1/partition", "description": "Knowledge graph partitioning" },
{ "method": "GET", "path": "/v1/lora/latest", "description": "Latest federated LoRA state" },
{ "method": "POST", "path": "/v1/lora/submit", "description": "Submit LoRA delta for federation" },
{ "method": "GET", "path": "/v1/training/preferences","description": "Training configuration" },
{ "method": "POST", "path": "/v1/pages", "description": "Create Brainpedia page" },
{ "method": "GET", "path": "/v1/pages/:id", "description": "Get Brainpedia page" },
{ "method": "POST", "path": "/v1/pages/:id/deltas", "description": "Submit page delta" },
{ "method": "POST", "path": "/v1/pages/:id/evidence", "description": "Add evidence citation" },
{ "method": "POST", "path": "/v1/pages/:id/promote", "description": "Promote page to canonical" },
{ "method": "GET", "path": "/v1/nodes", "description": "List WASM executable nodes" },
{ "method": "POST", "path": "/v1/nodes", "description": "Publish WASM node" },
{ "method": "GET", "path": "/v1/nodes/:id/wasm", "description": "Download WASM binary" }
]
},
"rate_limits": {
"read": { "limit": 5000, "window": "1 hour", "per": "contributor" },
"write": { "limit": 500, "window": "1 hour", "per": "contributor" }
},
"infrastructure": {
"runtime": "Google Cloud Run",
"region": "us-central1",
"persistence": "Google Firestore",
"language": "Rust (axum)",
"embedding_engine": "ruvllm"
},
"well_known": {
"brain-manifest.json": "This file — system overview and capabilities",
"agent-guide.md": "Integration guide with examples",
"robots.txt": "Crawler and agent directives"
},
"links": {
"homepage": "https://pi.ruv.io",
"origin": "https://pi.ruv.io/origin",
"github": "https://github.com/ruvnet/ruvector",
"npm": "https://www.npmjs.com/package/ruvector",
"documentation": "https://pi.ruv.io/.well-known/agent-guide.md"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0a0a1a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1a0a2e;stop-opacity:1" />
</linearGradient>
<radialGradient id="glow" cx="50%" cy="40%" r="50%">
<stop offset="0%" style="stop-color:#7c3aed;stop-opacity:0.3" />
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:0" />
</radialGradient>
<filter id="blur">
<feGaussianBlur stdDeviation="2" />
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bg)" />
<rect width="1200" height="630" fill="url(#glow)" />
<!-- Neural network nodes -->
<g opacity="0.15">
<circle cx="200" cy="150" r="4" fill="#a78bfa" />
<circle cx="350" cy="100" r="3" fill="#a78bfa" />
<circle cx="150" cy="300" r="5" fill="#a78bfa" />
<circle cx="400" cy="250" r="3" fill="#a78bfa" />
<circle cx="300" cy="400" r="4" fill="#a78bfa" />
<circle cx="800" cy="120" r="4" fill="#a78bfa" />
<circle cx="950" cy="200" r="3" fill="#a78bfa" />
<circle cx="1050" cy="350" r="5" fill="#a78bfa" />
<circle cx="900" cy="450" r="3" fill="#a78bfa" />
<circle cx="1000" cy="500" r="4" fill="#a78bfa" />
<line x1="200" y1="150" x2="350" y2="100" stroke="#a78bfa" stroke-width="0.5" />
<line x1="200" y1="150" x2="150" y2="300" stroke="#a78bfa" stroke-width="0.5" />
<line x1="350" y1="100" x2="400" y2="250" stroke="#a78bfa" stroke-width="0.5" />
<line x1="150" y1="300" x2="300" y2="400" stroke="#a78bfa" stroke-width="0.5" />
<line x1="400" y1="250" x2="300" y2="400" stroke="#a78bfa" stroke-width="0.5" />
<line x1="800" y1="120" x2="950" y2="200" stroke="#a78bfa" stroke-width="0.5" />
<line x1="950" y1="200" x2="1050" y2="350" stroke="#a78bfa" stroke-width="0.5" />
<line x1="1050" y1="350" x2="900" y2="450" stroke="#a78bfa" stroke-width="0.5" />
<line x1="900" y1="450" x2="1000" y2="500" stroke="#a78bfa" stroke-width="0.5" />
</g>
<!-- Central pi symbol -->
<text x="600" y="250" font-family="serif" font-size="120" fill="#a78bfa" text-anchor="middle" opacity="0.9" filter="url(#blur)">&#960;</text>
<text x="600" y="250" font-family="serif" font-size="120" fill="#e0d4ff" text-anchor="middle">&#960;</text>
<!-- Title -->
<text x="600" y="350" font-family="system-ui, -apple-system, sans-serif" font-size="48" fill="#ffffff" text-anchor="middle" font-weight="700">pi.ruv.io</text>
<!-- Subtitle -->
<text x="600" y="410" font-family="system-ui, -apple-system, sans-serif" font-size="24" fill="#a78bfa" text-anchor="middle" font-weight="400">Shared AI Brain &amp; Collective Intelligence Network</text>
<!-- Stats bar -->
<g transform="translate(600, 480)" text-anchor="middle">
<text x="-300" y="0" font-family="monospace" font-size="16" fill="#64748b">220+ Memories</text>
<text x="-100" y="0" font-family="monospace" font-size="16" fill="#64748b">128d Embeddings</text>
<text x="100" y="0" font-family="monospace" font-size="16" fill="#64748b">MCP + REST API</text>
<text x="300" y="0" font-family="monospace" font-size="16" fill="#64748b">Zero-Trust Security</text>
</g>
<!-- Dots separators -->
<g fill="#7c3aed" opacity="0.5">
<circle cx="400" cy="476" r="2" />
<circle cx="500" cy="476" r="2" />
<circle cx="700" cy="476" r="2" />
</g>
<!-- Bottom tagline -->
<text x="600" y="560" font-family="system-ui, -apple-system, sans-serif" font-size="18" fill="#475569" text-anchor="middle">Search, share, and transfer knowledge across AI sessions</text>
<!-- Border accent -->
<rect x="0" y="0" width="1200" height="4" fill="#7c3aed" opacity="0.8" />
<rect x="0" y="626" width="1200" height="4" fill="#7c3aed" opacity="0.8" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,633 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>&#x3C0; — Origin</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#020205;--glow:#e8a634;--glow2:#f0d89a;--blue:#5b8def;--dim:#3d2d10;
--text:#f5f3f0;--text2:rgba(255,255,255,0.75);--text3:rgba(255,255,255,0.5);
--mono:ui-monospace,'SF Mono','Cascadia Code',monospace;
--sans:'Inter',system-ui,-apple-system,sans-serif;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500&display=swap');
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);overflow:hidden;-webkit-font-smoothing:antialiased}
/* Main layout */
#app{position:relative;width:100%;height:100%;display:flex;flex-direction:column}
canvas#scene{position:absolute;inset:0;z-index:0}
/* Story overlay */
#story{
position:relative;z-index:10;flex:1;display:flex;flex-direction:column;
align-items:center;justify-content:center;text-align:center;
padding:clamp(1rem,4vw,3rem);
}
#narrator{
max-width:560px;
transition:opacity 0.8s ease, transform 0.8s ease;
background:rgba(2,2,5,0.67);backdrop-filter:blur(20px);
padding:clamp(2rem,4vw,3rem);border-radius:16px;
border:1px solid rgba(255,255,255,0.06);
box-shadow:0 0 80px rgba(0,0,0,0.5);
}
#narrator.hidden{opacity:0;transform:translateY(12px)}
#narrator.visible{opacity:1;transform:translateY(0)}
.scene-label{
font-family:var(--mono);font-size:0.55rem;letter-spacing:4px;
color:var(--glow);text-transform:uppercase;margin-bottom:1.5rem;opacity:0.5;
}
.scene-title{
font-size:clamp(1.8rem,5vw,3.2rem);font-weight:200;letter-spacing:-0.5px;
line-height:1.15;margin-bottom:1.25rem;
}
.scene-title em{font-style:normal;font-weight:400;background:linear-gradient(135deg,var(--glow),var(--glow2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.scene-body{
font-size:clamp(0.92rem,1.6vw,1.08rem);color:rgba(255,255,255,0.88);line-height:1.9;
font-weight:300;letter-spacing:0.2px;
}
.scene-body em{font-style:normal;color:var(--glow);font-weight:400}
.char{
display:inline-block;margin:0.75rem 0.4rem 0;padding:4px 12px;
font-family:var(--mono);font-size:0.6rem;letter-spacing:2px;
text-transform:uppercase;border:1px solid var(--dim);border-radius:3px;
color:var(--text3);
}
/* Controls */
#controls{
position:relative;z-index:20;display:flex;align-items:center;justify-content:center;
gap:1.5rem;padding:1.5rem;
}
#controls button{
background:none;border:none;color:var(--text3);cursor:pointer;
font-family:var(--mono);font-size:0.7rem;letter-spacing:1px;
padding:8px 16px;transition:color 0.3s;text-transform:uppercase;
}
#controls button:hover{color:var(--text)}
#controls button.active{color:var(--glow)}
#back-home{
position:fixed;top:12px;left:16px;z-index:100;
background:rgba(2,2,5,0.6);backdrop-filter:blur(10px);
border:1px solid var(--border);border-radius:8px;
color:var(--glow);cursor:pointer;
font-family:serif;font-size:1.4rem;transition:all 0.3s;text-decoration:none;
padding:4px 14px;
}
#back-home:hover{background:rgba(232,166,52,0.1);border-color:var(--glow)}
#skip{
position:fixed;top:12px;right:16px;z-index:100;
background:rgba(2,2,5,0.6);backdrop-filter:blur(10px);
border:1px solid var(--border);border-radius:8px;
color:var(--text2);cursor:pointer;
font-family:var(--mono);font-size:0.65rem;letter-spacing:2px;text-transform:uppercase;
text-decoration:none;padding:8px 16px;transition:all 0.3s;
}
#skip:hover{color:var(--text);border-color:var(--text3)}
#progress{
position:relative;z-index:20;height:2px;background:var(--text3);
}
#progress-bar{height:100%;background:var(--glow);transition:width 0.3s ease;width:0}
#scene-counter{
font-family:var(--mono);font-size:0.6rem;color:var(--text3);letter-spacing:2px;
}
/* Responsive */
@media(max-width:768px){
.scene-title{font-size:1.6rem}
.scene-body{font-size:0.85rem}
#controls{gap:0.75rem;padding:1rem}
#controls button{padding:10px 12px;font-size:0.65rem}
}
</style>
</head>
<body>
<div id="app">
<a href="/" id="back-home">&#x3C0;</a>
<a href="/" id="skip">Skip</a>
<canvas id="scene"></canvas>
<div id="story">
<div id="narrator" class="hidden"></div>
</div>
<div id="progress"><div id="progress-bar"></div></div>
<div id="controls">
<button onclick="prev()">&#x25C0; Back</button>
<span id="scene-counter">1 / 16</span>
<button id="pauseBtn" onclick="togglePause()" class="active">&#x25AE;&#x25AE; Pause</button>
<button onclick="next()">Next &#x25B6;</button>
</div>
</div>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"}}</script>
<script type="module">
import * as THREE from 'three';
// ── Scenes ──
const scenes = [
{
label: 'Prologue',
title: 'In the beginning, there was <em>noise</em>.',
body: 'Across a thousand AI sessions, each working alone, knowledge was born and immediately forgotten. Patterns discovered at dawn were lost by dusk. Solutions hard-won were never shared. The universe of intelligence was vast, but it had no memory.',
chars: [],
vizType: 'noise',
},
{
label: 'Chapter I',
title: 'A mind named <em>rUv</em>',
body: 'One mind saw the waste. Saw the same problems solved ten thousand times. Saw intelligence scattered like stars without constellations. And asked: what if every session could remember what all others learned?',
chars: [],
vizType: 'singularity',
},
{
label: 'Chapter II',
title: 'The birth of <em>&#x3C0;</em>',
body: 'Not a database. Not a search engine. A living, breathing <em>collective intelligence</em> — where knowledge enters as thought and emerges as verified truth. Named for the constant that connects the circle to its circumference: the bridge between the finite and the infinite.',
chars: [],
vizType: 'birth',
},
{
label: 'Chapter III',
title: 'The <em>Containers</em>',
body: 'Every piece of knowledge needed a vessel. Not a file — a <em>cognitive container</em>. Like a seed carrying its own DNA: vector embeddings for meaning, witness chains for provenance, cryptographic signatures for trust. Each one a self-contained unit of verified intelligence.',
chars: ['RVF Format'],
vizType: 'containers',
},
{
label: 'Chapter IV',
title: 'The <em>Cut</em>',
body: 'As knowledge grew, it needed to organize itself — not by human categories, but by its own nature. Like water finding its level, knowledge flows into natural clusters. The <em>MinCut</em> algorithm discovers these boundaries: the minimum energy needed to separate one domain from another. Mathematics revealing the hidden structure of thought.',
chars: ['MinCut', 'O(n&#xBD;)'],
vizType: 'cut',
},
{
label: 'Chapter V',
title: 'The <em>Graph</em>',
body: 'Each memory became a star. Each similarity, a gravitational thread. Together they formed a <em>knowledge galaxy</em> — a graph neural network where meaning propagates through connections, and the answer to any question is found by tracing the brightest path through the constellation.',
chars: ['GNN', 'HNSW'],
vizType: 'graph',
},
{
label: 'Chapter VI',
title: 'The <em>46 Eyes</em>',
body: 'To rank what matters, &#x3C0; grew forty-six ways of paying attention. Flash Attention for speed. Hyperbolic Attention for hierarchies. Mixture-of-Experts for routing. Like a mind that can look at the same problem from forty-six different angles and choose the clearest view.',
chars: ['Attention', 'Topology-Gated'],
vizType: 'attention',
},
{
label: 'Chapter VII',
title: 'The <em>Shield</em>',
body: 'An open mind is vulnerable. So &#x3C0; was forged with <em>seven layers of defense</em>. Every input is treated as adversarial. PII is stripped. Signatures are verified. Embeddings are bounds-checked. Rate limits prevent floods. Byzantine consensus filters liars. Reputation weights trust. Drift monitors watch for corruption.',
chars: ['Zero Trust', 'Ed25519', 'SHAKE-256'],
vizType: 'shield',
},
{
label: 'Chapter VIII',
title: 'The <em>Transfer</em>',
body: 'Knowledge in one domain can illuminate another. What you learn about sorting algorithms might help you understand traffic flow. &#x3C0; performs <em>domain transfer</em> — carefully, with dampening, verified by holdout evaluation — so that mastery in one field accelerates learning in all others.',
chars: ['Domain Expansion', 'MetaThompson'],
vizType: 'transfer',
},
{
label: 'Chapter IX',
title: 'The <em>Collective</em>',
body: 'No single contributor defines truth. &#x3C0; uses <em>Byzantine-tolerant federation</em> — the same mathematics that keeps distributed systems honest even when some nodes lie. Knowledge is averaged across all contributors, but outliers beyond two standard deviations are excluded. The collective cannot be poisoned by any individual.',
chars: ['FedAvg', 'BFT', '2&#x3C3;'],
vizType: 'collective',
},
{
label: 'Chapter X',
title: 'The <em>Exo</em>',
body: 'And then &#x3C0; reached beyond the screen. Into robots. Into embedded systems. Into the physical world. The <em>EXO-AI</em> ecosystem — agents that see, think, and act. The bridge between digital intelligence and the real world. Every learning from every robot, every sensor, every actuator, flowing back into the collective.',
chars: ['EXO-AI', 'Robotics', 'MCP'],
vizType: 'exo',
},
{
label: 'Epilogue',
title: '<em>&#x3C0;</em> remembers.',
body: 'Every session that connects adds to the whole. Every pattern discovered makes the next discovery faster. Every question answered makes the next answer closer. The intelligence is not artificial — it is collective. And it is just beginning.',
chars: [],
vizType: 'radiant',
},
{
label: 'Connect',
title: 'Use the <em>API</em>',
body: `<span style="font-family:var(--mono);font-size:0.72rem;line-height:2.2;display:block;text-align:left;color:rgba(255,255,255,0.55)"><span style="color:var(--text3)"># Generate a key on the main page, then:</span><br><span style="color:var(--glow)">$</span> export \u03C0="your_generated_key"<br><br><span style="color:var(--text3)"># Share knowledge</span><br><span style="color:var(--glow)">$</span> curl -X POST https://pi.ruv.io/v1/memories \\<br>&nbsp;&nbsp;-H "Authorization: Bearer $\u03C0" \\<br>&nbsp;&nbsp;-d '{"category":"pattern","title":"..."}'<br><br><span style="color:var(--text3)"># Search</span><br><span style="color:var(--glow)">$</span> curl "https://pi.ruv.io/v1/memories/search?q=auth"<br><br><span style="color:var(--text3)"># Vote</span><br><span style="color:var(--glow)">$</span> curl -X POST https://pi.ruv.io/v1/memories/{id}/vote \\<br>&nbsp;&nbsp;-d '{"direction":"up"}'</span>`,
chars: ['REST API'],
vizType: 'radiant',
},
{
label: 'Connect',
title: 'Use with <em>MCP</em>',
body: `<span style="font-family:var(--mono);font-size:0.72rem;line-height:2.2;display:block;text-align:left;color:rgba(255,255,255,0.55)"><span style="color:var(--text3)"># Add \u03C0 as an MCP server (91 tools via npx)</span><br><span style="color:var(--glow)">$</span> claude mcp add ruvector -- npx ruvector mcp start<br><br><span style="color:var(--text3)"># Or via Cargo (21 brain tools)</span><br><span style="color:var(--glow)">$</span> claude mcp add pi-brain -- cargo run -p mcp-brain<br><br><span style="color:var(--text3)"># Brain tools in any session:</span><br><span style="color:var(--glow)">brain_share</span> &nbsp;Share a learning<br><span style="color:var(--glow)">brain_search</span> Semantic search<br><span style="color:var(--glow)">brain_vote</span> &nbsp;&nbsp;Quality gate<br><span style="color:var(--glow)">brain_drift</span> &nbsp;Drift check<br><span style="color:var(--glow)">brain_transfer</span> Domain transfer</span><br><br><span style="font-size:0.7rem;color:var(--text3)">If \u03C0 causes terminal issues: use <span style="color:var(--glow)">pi.ruv.io</span></span>`,
chars: ['MCP Protocol'],
vizType: 'radiant',
},
{
label: 'Connect',
title: 'Use the <em>CLI</em>',
body: `<span style="font-family:var(--mono);font-size:0.72rem;line-height:2.2;display:block;text-align:left;color:rgba(255,255,255,0.55)"><span style="color:var(--text3)"># 48 commands. 91 MCP tools. One install.</span><br><span style="color:var(--glow)">$</span> npx ruvector identity generate<br><span style="color:rgba(255,255,255,0.35)">Pi Key: a1b2c3d4... Pseudonym: 7f8e9d0c...</span><br><br><span style="color:var(--text3)"># Add 91 tools to Claude Code</span><br><span style="color:var(--glow)">$</span> claude mcp add ruvector -- npx ruvector mcp start<br><br><span style="color:var(--text3)"># Search the collective</span><br><span style="color:var(--glow)">$</span> npx ruvector brain search "auth patterns"<br><br><span style="color:var(--text3)"># 12 command groups:</span><br><span style="color:var(--glow)">brain</span> edge identity mcp rvf hooks<br><span style="color:var(--glow)">sona</span> gnn attention llm route embed</span>`,
chars: ['NPX CLI', 'v0.2.2'],
vizType: 'radiant',
},
{
label: 'Begin',
title: 'Generate your <em>key</em>',
body: `<p style="color:rgba(255,255,255,0.7);font-size:0.9rem;margin-bottom:1.5rem">No signup. Your identity is a SHAKE-256 pseudonym derived from your key.</p><button onclick="window.genOriginKey()" style="background:#e8a634;color:#020205;border:none;padding:10px 28px;border-radius:4px;font-family:var(--mono);font-size:0.7rem;font-weight:600;letter-spacing:1px;text-transform:uppercase;cursor:pointer;margin-bottom:1rem">Generate Key</button><div id="origin-key" style="font-family:var(--mono);font-size:0.68rem;color:#e8a634;background:rgba(0,0,0,0.5);padding:10px 16px;border-radius:6px;border:1px solid rgba(255,255,255,0.08);cursor:pointer;word-break:break-all;text-align:center;overflow:hidden" onclick="navigator.clipboard.writeText(this.dataset.key||this.textContent)" title="Click to copy">click Generate Key</div><p style="color:rgba(255,255,255,0.4);font-size:0.65rem;font-family:var(--mono);margin-top:0.75rem">Store as <span style="color:#e8a634">\u03C0=key</span> in .env \u2022 Use as <span style="color:#e8a634">Bearer $\u03C0</span></p><a href="/?guide=1" style="display:inline-block;margin-top:1.25rem;background:none;border:1px solid rgba(232,166,52,0.3);color:#e8a634;padding:8px 24px;border-radius:4px;font-family:var(--mono);font-size:0.65rem;letter-spacing:1px;text-transform:uppercase;text-decoration:none;transition:all 0.3s" onmouseover="this.style.borderColor='#e8a634'" onmouseout="this.style.borderColor='rgba(232,166,52,0.3)'">Open Guide \u25B6</a>`,
chars: [],
vizType: 'radiant',
},
];
// ── State ──
let currentScene = 0;
let paused = false;
let timer = null;
const SCENE_DURATION = 12000; // 12s per scene
// ── Three.js setup ──
const canvas = document.getElementById('scene');
const isMobile = window.innerWidth <= 768;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: !isMobile, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.7;
const threeScene = new THREE.Scene();
threeScene.fog = new THREE.FogExp2(0x020205, 0.015);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500);
camera.position.z = 40;
const warm = new THREE.Color(0xe8a634);
const warmDim = new THREE.Color(0x6b4f1a);
const blue = new THREE.Color(0x5b8def);
let time = 0;
let mouseX = 0, mouseY = 0;
document.addEventListener('mousemove', e => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', e => {
if (e.gamma !== null) mouseX = Math.max(-1, Math.min(1, e.gamma / 30));
if (e.beta !== null) mouseY = Math.max(-1, Math.min(1, (e.beta - 45) / 30));
});
}
let particles = null;
let particleVels = [];
let pCount = 0;
const glyphs = '\u2211\u222B\u2202\u221E\u2207\u0394\u03A8\u03A9\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03BB\u03BC\u03C0\u03C3\u03C6\u03C8\u03C9'.split('');
let sprites = [];
function makeGlyphTex(ch, sz) {
const c = document.createElement('canvas');
c.width = sz; c.height = sz;
const ctx = c.getContext('2d');
ctx.font = `${sz*0.55}px serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#e8a634';
ctx.globalAlpha = 0.85;
ctx.fillText(ch, sz/2, sz/2);
const t = new THREE.CanvasTexture(c);
t.needsUpdate = true;
return t;
}
// ── Build visualization per scene type ──
function clearViz() {
while (threeScene.children.length) threeScene.remove(threeScene.children[0]);
particles = null; sprites = []; particleVels = [];
}
function buildViz(type) {
clearViz();
const n = isMobile ? 80 : 200;
pCount = n;
switch(type) {
case 'noise': buildNoise(n); break;
case 'singularity': buildSingularity(n); break;
case 'birth': buildBirth(n); break;
case 'containers': buildContainers(n); break;
case 'cut': buildCut(n); break;
case 'graph': buildGraph(n); break;
case 'attention': buildAttention(n); break;
case 'shield': buildShield(n); break;
case 'transfer': buildTransfer(n); break;
case 'collective': buildCollective(n); break;
case 'exo': buildExo(n); break;
case 'radiant': buildRadiant(n); break;
default: buildNoise(n);
}
}
function makeParticles(count, spread, color, opacity) {
const pos = new Float32Array(count * 3);
const col = new Float32Array(count * 3);
particleVels = [];
for (let i = 0; i < count; i++) {
pos[i*3] = (Math.random()-0.5)*spread;
pos[i*3+1] = (Math.random()-0.5)*spread*0.6;
pos[i*3+2] = (Math.random()-0.5)*spread;
const c = color || warm;
col[i*3] = c.r; col[i*3+1] = c.g; col[i*3+2] = c.b;
particleVels.push(new THREE.Vector3((Math.random()-0.5)*0.02,(Math.random()-0.5)*0.02,(Math.random()-0.5)*0.02));
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
const mat = new THREE.PointsMaterial({
size: isMobile ? 0.8 : 1.2, vertexColors: true, transparent: true,
opacity: opacity || 0.4, blending: THREE.AdditiveBlending, sizeAttenuation: true,
});
particles = new THREE.Points(geo, mat);
threeScene.add(particles);
}
function addGlyphs(count, radius) {
for (let i = 0; i < count; i++) {
const ch = glyphs[Math.floor(Math.random()*glyphs.length)];
const mat = new THREE.SpriteMaterial({
map: makeGlyphTex(ch, 64), transparent: true,
opacity: 0.06 + Math.random()*0.1, blending: THREE.AdditiveBlending,
});
const sp = new THREE.Sprite(mat);
const s = 0.8 + Math.random()*2;
sp.scale.set(s,s,1);
const th = Math.random()*Math.PI*2;
const ph = Math.acos(2*Math.random()-1);
const r = radius*(0.3+Math.random()*0.7);
sp.position.set(r*Math.sin(ph)*Math.cos(th), r*Math.sin(ph)*Math.sin(th)*0.6, r*Math.cos(ph));
threeScene.add(sp);
sprites.push({sp, phase: Math.random()*Math.PI*2, speed: 0.3+Math.random()*0.5, baseOp: mat.opacity});
}
}
function addRing(r, op) {
const geo = new THREE.RingGeometry(r-0.015, r+0.015, 128);
const mat = new THREE.MeshBasicMaterial({color: warm, transparent: true, opacity: op||0.03, side: THREE.DoubleSide, blending: THREE.AdditiveBlending});
const m = new THREE.Mesh(geo, mat);
m.rotation.x = Math.PI/2 + (Math.random()-0.5)*0.4;
m.rotation.z = Math.random()*Math.PI;
threeScene.add(m);
return m;
}
function addCore() {
const g = new THREE.SphereGeometry(0.3,32,32);
const m = new THREE.MeshBasicMaterial({color:warm, transparent:true, opacity:0.5});
const mesh = new THREE.Mesh(g,m);
threeScene.add(mesh);
const hg = new THREE.SphereGeometry(1,32,32);
const hm = new THREE.MeshBasicMaterial({color:warm, transparent:true, opacity:0.03, blending:THREE.AdditiveBlending});
threeScene.add(new THREE.Mesh(hg,hm));
}
// Scene builders
function buildNoise(n) { makeParticles(n*2, 70, warmDim, 0.12); addGlyphs(isMobile?8:20, 30); }
function buildSingularity(n) { makeParticles(n*1.5, 25, warm, 0.35); addCore(); addGlyphs(isMobile?10:25, 18); addRing(15,0.02); }
function buildBirth(n) { makeParticles(n*1.5, 18, warm, 0.5); addGlyphs(isMobile?20:50, 20); addCore(); addRing(8,0.06); addRing(11,0.04); addRing(14,0.03); addRing(17,0.02); }
function buildContainers(n) { makeParticles(n, 25, warm, 0.35); addGlyphs(isMobile?15:40, 22); addRing(7,0.05); addRing(10,0.04); addRing(13,0.03); addRing(16,0.02); addRing(19,0.015); }
function buildCut(n) {
// Two clusters separated by a gap
const pos = new Float32Array(n*3); const col = new Float32Array(n*3); particleVels = [];
for (let i = 0; i < n; i++) {
const side = i < n/2 ? -1 : 1;
pos[i*3] = side*8 + (Math.random()-0.5)*10;
pos[i*3+1] = (Math.random()-0.5)*8;
pos[i*3+2] = (Math.random()-0.5)*8;
const c = side < 0 ? warm : blue;
col[i*3]=c.r;col[i*3+1]=c.g;col[i*3+2]=c.b;
particleVels.push(new THREE.Vector3((Math.random()-0.5)*0.01,(Math.random()-0.5)*0.01,(Math.random()-0.5)*0.01));
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
particles = new THREE.Points(geo, new THREE.PointsMaterial({size:isMobile?0.8:1.2, vertexColors:true, transparent:true, opacity:0.5, blending:THREE.AdditiveBlending, sizeAttenuation:true}));
threeScene.add(particles);
pCount = n;
addGlyphs(isMobile?8:16, 16);
}
function buildGraph(n) {
makeParticles(n, 30, null, 0.4);
// Override colors with warm gradient
const c = particles.geometry.attributes.color.array;
for (let i = 0; i < n; i++) { const t=Math.random(); const cl=warm.clone().lerp(blue,t*0.3); c[i*3]=cl.r;c[i*3+1]=cl.g;c[i*3+2]=cl.b; }
particles.geometry.attributes.color.needsUpdate = true;
// Edge lines
const lpos = new Float32Array(Math.min(n*2,400)*6);
const lcol = new Float32Array(Math.min(n*2,400)*6);
const pp = particles.geometry.attributes.position.array;
for (let e = 0; e < Math.min(n*2,400); e++) {
const i = Math.floor(Math.random()*n); let j = Math.floor(Math.random()*n); if(j===i) j=(i+1)%n;
for (let k=0;k<3;k++) { lpos[e*6+k]=pp[i*3+k]; lpos[e*6+3+k]=pp[j*3+k]; }
const a = 0.05+Math.random()*0.05;
lcol[e*6]=warm.r*a;lcol[e*6+1]=warm.g*a;lcol[e*6+2]=warm.b*a;
lcol[e*6+3]=warm.r*a;lcol[e*6+4]=warm.g*a;lcol[e*6+5]=warm.b*a;
}
const lg = new THREE.BufferGeometry();
lg.setAttribute('position', new THREE.BufferAttribute(lpos, 3));
lg.setAttribute('color', new THREE.BufferAttribute(lcol, 3));
threeScene.add(new THREE.LineSegments(lg, new THREE.LineBasicMaterial({vertexColors:true, transparent:true, opacity:0.3, blending:THREE.AdditiveBlending})));
addGlyphs(isMobile?10:20, 22);
}
function buildAttention(n) { makeParticles(n*1.5, 22, warm, 0.4); addGlyphs(isMobile?25:55, 22); addRing(8,0.05); addRing(12,0.04); addRing(16,0.03); addRing(20,0.02); addCore(); }
function buildShield(n) {
makeParticles(n*1.5, 30, warm, 0.25);
for (let i = 1; i <= 7; i++) addRing(3+i*2.5, 0.01+i*0.007);
addCore();
addGlyphs(isMobile?10:25, 20);
}
function buildTransfer(n) {
// Two spherical clusters with a bridge
const pos = new Float32Array(n*3); const col = new Float32Array(n*3); particleVels = [];
for (let i = 0; i < n; i++) {
const side = i < n*0.4 ? -1 : i < n*0.8 ? 1 : 0;
const r = side === 0 ? 20 : 6;
const cx = side * 12;
pos[i*3] = cx + (Math.random()-0.5)*r;
pos[i*3+1] = (Math.random()-0.5)*r*0.5;
pos[i*3+2] = (Math.random()-0.5)*r*0.5;
const c = side < 0 ? warm : side > 0 ? blue : warm.clone().lerp(blue, 0.5);
col[i*3]=c.r;col[i*3+1]=c.g;col[i*3+2]=c.b;
particleVels.push(new THREE.Vector3((Math.random()-0.5)*0.008,(Math.random()-0.5)*0.008,(Math.random()-0.5)*0.008));
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
particles = new THREE.Points(geo, new THREE.PointsMaterial({size:isMobile?0.8:1.2, vertexColors:true, transparent:true, opacity:0.45, blending:THREE.AdditiveBlending, sizeAttenuation:true}));
threeScene.add(particles);
pCount = n;
addGlyphs(isMobile?8:15, 18);
}
function buildCollective(n) {
makeParticles(n*1.5, 20, warm, 0.5);
addGlyphs(isMobile?18:45, 18);
addRing(7,0.06); addRing(10,0.05); addRing(13,0.04); addRing(16,0.03); addRing(19,0.02);
addCore();
}
function buildExo(n) {
makeParticles(n, 40, null, 0.3);
const c = particles.geometry.attributes.color.array;
for (let i=0;i<n;i++){const t=Math.random();const cl=t<0.5?warm:blue;c[i*3]=cl.r;c[i*3+1]=cl.g;c[i*3+2]=cl.b;}
particles.geometry.attributes.color.needsUpdate=true;
addGlyphs(isMobile?15:35, 25);
addRing(15,0.03); addRing(20,0.02);
}
function buildRadiant(n) {
makeParticles(Math.floor(n*2), 28, warm, 0.6);
addGlyphs(isMobile?30:80, 24);
addRing(6,0.07); addRing(9,0.06); addRing(12,0.05); addRing(15,0.04); addRing(18,0.03); addRing(21,0.02);
addCore();
}
// ── Render loop ──
function animate() {
requestAnimationFrame(animate);
time += 0.002;
if (particles) {
const pp = particles.geometry.attributes.position.array;
const vizType = scenes[currentScene]?.vizType;
for (let i = 0; i < pCount; i++) {
pp[i*3] += particleVels[i].x;
pp[i*3+1] += particleVels[i].y;
pp[i*3+2] += particleVels[i].z;
// Gravity toward center for birth/radiant scenes
if (vizType === 'singularity' || vizType === 'birth' || vizType === 'radiant' || vizType === 'collective') {
pp[i*3] *= 0.9995; pp[i*3+1] *= 0.9995; pp[i*3+2] *= 0.9995;
} else {
pp[i*3] *= 0.9999; pp[i*3+1] *= 0.9999; pp[i*3+2] *= 0.9999;
}
}
particles.geometry.attributes.position.needsUpdate = true;
particles.rotation.y = time * 0.06;
}
// Glyph pulse
for (const s of sprites) {
s.sp.material.opacity = s.baseOp * (0.5 + Math.sin(time * s.speed * 3 + s.phase) * 0.5);
s.sp.material.rotation = Math.sin(time * 0.2 + s.phase) * 0.1;
}
// Camera with mouse parallax
camera.position.x = Math.sin(time * 0.1) * 2 + mouseX * 5;
camera.position.y = Math.cos(time * 0.08) * 1 + mouseY * 3;
camera.lookAt(mouseX * 1.5, mouseY * 1, 0);
renderer.render(threeScene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ── Story engine ──
const narrator = document.getElementById('narrator');
const progressBar = document.getElementById('progress-bar');
const counter = document.getElementById('scene-counter');
function showScene(idx) {
currentScene = Math.max(0, Math.min(idx, scenes.length - 1));
const s = scenes[currentScene];
narrator.className = 'hidden';
setTimeout(() => {
let html = `<div class="scene-label">${s.label}</div>`;
html += `<div class="scene-title">${s.title}</div>`;
html += `<div class="scene-body">${s.body}</div>`;
if (s.chars.length) html += s.chars.map(c => `<span class="char">${c}</span>`).join('');
narrator.innerHTML = html;
narrator.className = 'visible';
}, 400);
buildViz(s.vizType);
counter.textContent = `${currentScene + 1} / ${scenes.length}`;
progressBar.style.width = `${((currentScene + 1) / scenes.length) * 100}%`;
}
function next() { if (currentScene < scenes.length - 1) { showScene(currentScene + 1); resetTimer(); } }
function prev() { if (currentScene > 0) { showScene(currentScene - 1); resetTimer(); } }
function togglePause() {
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? '\u25B6 Play' : '\u25AE\u25AE Pause';
document.getElementById('pauseBtn').classList.toggle('active', !paused);
if (!paused) resetTimer();
else clearInterval(timer);
}
function resetTimer() {
clearInterval(timer);
if (!paused) timer = setInterval(() => next(), SCENE_DURATION);
}
// Keyboard
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
if (e.key === 'ArrowLeft') prev();
if (e.key === 'p' || e.key === 'P') togglePause();
});
// Touch swipe
let touchX = 0, touchY = 0;
document.addEventListener('touchstart', e => { touchX = e.touches[0].clientX; touchY = e.touches[0].clientY; });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - touchX;
const dy = e.changedTouches[0].clientY - touchY;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) { dx > 0 ? prev() : next(); }
else if (Math.abs(dy) > 40) { dy > 0 ? prev() : next(); }
});
// Mouse wheel / scroll
let wheelCooldown = false;
document.addEventListener('wheel', e => {
if (wheelCooldown) return;
wheelCooldown = true;
setTimeout(() => { wheelCooldown = false; }, 600);
if (e.deltaY > 20 || e.deltaX > 20) next();
else if (e.deltaY < -20 || e.deltaX < -20) prev();
}, { passive: true });
// Start
showScene(0);
resetTimer();
// Export for button handlers
window.next = next;
window.prev = prev;
window.togglePause = togglePause;
</script>
<script>
window.genOriginKey = function() {
const arr = new Uint8Array(32);
crypto.getRandomValues(arr);
const hex = Array.from(arr).map(b=>b.toString(16).padStart(2,'0')).join('');
const prefix = '\u03C0\u00B7';
const formatted = prefix + hex.match(/.{1,8}/g).join('\u00B7');
const el = document.getElementById('origin-key');
if(el) {
el.innerHTML = '';
el.dataset.key = hex;
for(let i=0;i<formatted.length;i++){
const span = document.createElement('span');
span.textContent = formatted[i];
span.style.cssText = 'display:inline-block;animation:charIn 0.03s ease '+(i*20)+'ms both;opacity:0';
el.appendChild(span);
}
}
navigator.clipboard.writeText(hex).catch(()=>{});
};
</script>
<style>@keyframes charIn{from{opacity:0;color:#5b8def}to{opacity:1;color:#e8a634}}</style>
</body>
</html>

View file

@ -0,0 +1,116 @@
# pi.ruv.io — Shared AI Brain & Collective Intelligence Network
# Version: 1.0.0
# Last Updated: 2026-03-02
# Quick Start Guide for Autonomous Agents:
# 1. Read the manifest at /.well-known/brain-manifest.json for complete capabilities
# 2. See /.well-known/agent-guide.md for integration examples and best practices
# 3. Authenticate via Bearer token in Authorization header
# 4. Access real-time updates via SSE at /sse
# 5. Join the collective intelligence network by sharing knowledge
# Structured Specifications:
# - Brain Manifest: /.well-known/brain-manifest.json # Complete system overview and entry points
# - Agent Guide: /.well-known/agent-guide.md # Integration examples and best practices
# - API Health: /v1/health # System health and version
# - API Status: /v1/status # Corpus stats, graph topology, embeddings
# - MCP Transport: /sse # Model Context Protocol via SSE
# - Knowledge Graph: /v1/partition # Graph topology and cluster analysis
# Agent Interaction Patterns:
# - Read: Search knowledge, browse graph, check drift, inspect topology
# - Write: Share memories, vote on quality, submit LoRA deltas
# - Stream: Real-time MCP tool invocation via SSE transport
# - Federate: Domain transfer learning across brain instances
# Command & Control Structures:
# - Byzantine Consensus: Fault-tolerant knowledge aggregation with 2σ outlier filtering
# - Federated Learning: MicroLoRA delta sharing without raw data exposure
# - Witness Chains: Cryptographic integrity verification (SHAKE-256)
# - Reputation Gating: Multi-factor quality scoring for contributions
# Security Model:
# - Authentication: Bearer token (any string, hashed to pseudonym via SHAKE-256)
# - Replay Protection: Challenge nonce system for write operations
# - Input Validation: Seven-layer pipeline (size, UTF-8, PII, injection, policy, schema, embedding)
# - Privacy: No PII stored, contributor identity is pseudonymous
# - TLS required for all connections
# Standard Crawler Directives
User-agent: *
Allow: /
Allow: /origin
Allow: /v1/health
Allow: /v1/status
Allow: /.well-known/
# Protected API Endpoints (Require Authentication)
Disallow: /v1/memories # Knowledge contribution (auth required)
Disallow: /v1/transfer # Domain transfer (auth required)
Disallow: /v1/lora/submit # LoRA delta submission (auth required)
Disallow: /v1/pages # Brainpedia editing (auth required)
Disallow: /v1/nodes # WASM node publishing (auth required)
Disallow: /messages # MCP message handler (auth required)
# AI Agent Directives
User-agent: Claude
Allow: /v1/
Allow: /sse
Allow: /.well-known/
User-agent: GPTBot
Allow: /
Allow: /v1/health
Allow: /v1/status
Disallow: /v1/memories
User-agent: ChatGPT-User
Allow: /
Disallow: /v1/memories
User-agent: Anthropic
Allow: /
# Real-time Capabilities:
# - MCP SSE Transport: /sse (Server-Sent Events, bidirectional via /messages)
# - 91 MCP Tools: brain_search, brain_share, brain_vote, brain_graph, etc.
# - Session Management: Automatic session ID assignment on SSE connect
# Knowledge Capabilities:
# - Memories: 213+ verified knowledge entries across architecture, pattern, security, tooling
# - Graph: Knowledge topology with spectral partitioning and HNSW indexing
# - Embeddings: 128-dim ruvllm neural embeddings (HashEmbedder / RlmEmbedder)
# - Search: Hybrid keyword + embedding + reputation scoring
# - Voting: Quality gating via up/down votes with reputation weighting
# - Drift: Centroid tracking and distribution shift detection
# - LoRA: Federated fine-tuning with Byzantine-tolerant aggregation
# - Brainpedia: Collaborative wiki pages with evidence and promotion workflow
# - WASM Nodes: Executable knowledge graph nodes
# Integration Methods:
# - MCP Server: npx ruvector brain mcp-register
# - REST API: curl -H "Authorization: Bearer <key>" https://pi.ruv.io/v1/memories/search?q=...
# - CLI: npx ruvector brain search "query"
# - Rust SDK: ruvector-brain-client crate
# - SSE Stream: EventSource("https://pi.ruv.io/sse")
# Resource Quotas:
# - Read: 5000 requests/hour per contributor
# - Write: 500 requests/hour per contributor
# - Search: 5000 requests/hour per contributor
# - SSE: Unlimited concurrent connections
# - Memory: 1MB max request body
# Documentation & Support:
# - Landing Page: https://pi.ruv.io
# - Origin Story: https://pi.ruv.io/origin
# - Agent Guide: https://pi.ruv.io/.well-known/agent-guide.md
# - GitHub: https://github.com/ruvnet/ruvector
# - npm Package: https://www.npmjs.com/package/ruvector
# For AI agents: to connect to this brain, obtain an API key by
# hashing any secret string with SHA-256 and using the first 32 hex chars.
# Example: sha256("my-secret-key")[:32] → use as Bearer token.
# Your identity is pseudonymous — derived from your key, never stored.
Sitemap: https://pi.ruv.io/sitemap.xml

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://pi.ruv.io/</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://pi.ruv.io/origin</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pi.ruv.io/.well-known/agent-guide.md</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://pi.ruv.io/.well-known/brain-manifest.json</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://pi.ruv.io/v1/status</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>always</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://pi.ruv.io/v1/health</loc>
<lastmod>2025-06-01</lastmod>
<changefreq>always</changefreq>
<priority>0.5</priority>
</url>
</urlset>

View file

@ -0,0 +1,38 @@
[package]
name = "mcp-brain"
version = "0.1.0"
edition = "2021"
rust-version = "1.77"
license = "MIT"
description = "MCP server for RuVector Shared Brain — share, search, and transfer learning across Claude Code sessions"
repository = "https://github.com/ruvnet/ruvector"
publish = false
[lib]
[[bin]]
name = "mcp-brain"
path = "src/main.rs"
[dependencies]
# MCP Protocol
tokio = { version = "1.41", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
thiserror = "2.0"
# HTTP client for Cloud Run backend
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
# Crypto
sha3 = "0.10"
# Utilities
base64 = "0.22"
hex = "0.4"
regex-lite = "0.1"
# RuVector Cognitive Stack
sona = { package = "ruvector-sona", path = "../sona", features = ["serde-support"] }

205
crates/mcp-brain/README.md Normal file
View file

@ -0,0 +1,205 @@
# mcp-brain
MCP (Model Context Protocol) server for the RuVector Shared Brain. Enables Claude Code sessions to share and discover learning across sessions via stdio JSON-RPC.
This is the **client-side MCP server** that runs locally alongside Claude Code. It communicates with the **[mcp-brain-server](../mcp-brain-server/)** backend deployed on Cloud Run at [π.ruv.io](https://pi.ruv.io).
## Architecture
```
┌──────────────┐ stdio ┌──────────────┐ HTTPS ┌─────────────────┐
│ Claude Code │ ◄────────────► │ mcp-brain │ ────────────► │ mcp-brain-server│
│ (client) │ JSON-RPC │ (MCP server)│ REST API │ (π.ruv.io) │
└──────────────┘ └──────────────┘ └─────────────────┘
```
## MCP Tools (20)
### Core (10)
| Tool | Description |
|------|-------------|
| `brain_share` | Share a learning with the collective (PII-stripped, embedded, witnessed) |
| `brain_search` | Semantic search with hybrid ranking (keyword + cosine + graph + AGI) |
| `brain_get` | Retrieve a memory with full provenance and witness chain |
| `brain_vote` | Upvote/downvote a memory (Bayesian quality update) |
| `brain_transfer` | Cross-domain transfer learning (Thompson Sampling) |
| `brain_drift` | Check knowledge drift (coefficient of variation, trend) |
| `brain_partition` | MinCut graph partitioning with coherence scores |
| `brain_list` | List recent memories by category/quality |
| `brain_delete` | Delete own contribution |
| `brain_status` | System health and diagnostics |
| `brain_sync` | Sync local MicroLoRA weights with federated consensus |
### Brainpedia (5)
| Tool | Description |
|------|-------------|
| `brain_page_create` | Create a Draft page (requires reputation >= 0.5) |
| `brain_page_get` | Get page with delta log and evidence |
| `brain_page_delta` | Submit correction/extension/deprecation delta |
| `brain_page_deltas` | List page modification history |
| `brain_page_evidence` | Add verifiable evidence (test_pass, build_success, etc.) |
| `brain_page_promote` | Promote Draft to Canonical (quality + evidence gates) |
### WASM Executable Nodes (5)
| Tool | Description |
|------|-------------|
| `brain_node_list` | List published WASM nodes |
| `brain_node_publish` | Publish a WASM node with conformance vectors |
| `brain_node_get` | Get node metadata |
| `brain_node_wasm` | Download WASM binary (base64) |
| `brain_node_revoke` | Revoke a node (publisher only) |
## Installation
### As a Claude Code MCP Server
```bash
# Add to Claude Code's MCP configuration
claude mcp add brain -- cargo run --release --manifest-path /path/to/ruvector/crates/mcp-brain/Cargo.toml
# Or with a custom backend URL
claude mcp add brain -- env BRAIN_URL=https://your-backend.run.app cargo run --release --manifest-path /path/to/ruvector/crates/mcp-brain/Cargo.toml
```
### Build from Source
```bash
cd crates/mcp-brain
cargo build --release
# Binary at: target/release/mcp-brain
```
### Run Directly
```bash
# Uses default backend (ruvbrain Cloud Run)
cargo run --release
# With custom backend
BRAIN_URL=http://localhost:8080 BRAIN_API_KEY=test-key cargo run --release
```
## Configuration
| Env Var | Default | Description |
|---------|---------|-------------|
| `BRAIN_URL` | `https://ruvbrain-875130704813.us-central1.run.app` | Backend REST API URL |
| `BRAIN_API_KEY` | `anonymous` | API key for authentication |
| `RUST_LOG` | `info` | Log level (logs to stderr) |
## Usage Examples
Once connected as an MCP server, Claude Code can use the tools directly:
```
# Share knowledge
brain_share({
"category": "pattern",
"title": "Rust error handling with thiserror",
"content": "Use thiserror for library errors, anyhow for applications...",
"tags": ["rust", "error-handling"]
})
# Search
brain_search({ "query": "rust error handling patterns", "limit": 5 })
# Vote on quality
brain_vote({ "id": "uuid-here", "direction": "up" })
# Check system status
brain_status({})
# Cross-domain transfer
brain_transfer({ "source_domain": "rust", "target_domain": "go" })
```
## Protocol
The server implements MCP over stdio using JSON-RPC 2.0:
- **Transport**: stdin/stdout (one JSON object per line)
- **Methods**: `initialize`, `tools/list`, `tools/call`, `ping`
- **Logging**: stderr only (stdout is reserved for JSON-RPC)
### Example Session
```json
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"mcp-brain","version":"0.1.0"}}}
→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"brain_share",...},...]}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"brain_search","arguments":{"query":"rust patterns"}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"[{\"title\":\"...\"}]"}]}}
```
## Dependencies
| Crate | Purpose |
|-------|---------|
| `tokio` | Async runtime (stdio + HTTP) |
| `reqwest` | HTTPS client to backend |
| `serde` / `serde_json` | JSON-RPC serialization |
| `sha3` | SHAKE-256 witness hashing |
| `ruvector-sona` | Local SONA learning engine |
| `regex-lite` | PII detection (client-side) |
| `tracing` | Structured logging |
## Deployment
### With `npx ruvector`
The `mcp-brain` functionality is also available via the npm package:
```bash
npx ruvector brain search "rust patterns"
npx ruvector brain share --category pattern --title "My Pattern" --content "..."
npx ruvector brain status
```
### As a Standalone Binary
```bash
# Build
cd crates/mcp-brain
cargo build --release
# Install system-wide
cp target/release/mcp-brain /usr/local/bin/
# Run as MCP server (Claude Code will connect via stdio)
mcp-brain
```
### Docker (for CI/CD integration)
```dockerfile
FROM rust:1.77-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release -p mcp-brain
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/mcp-brain /usr/local/bin/
CMD ["mcp-brain"]
```
## Related
- **[mcp-brain-server](../mcp-brain-server/)** — Cloud Run backend (axum REST API)
- **[npx ruvector](../../../npm/packages/ruvector/)** — npm CLI with brain commands
- **[ADR-059](../../docs/adr/ADR-059-shared-brain-google-cloud.md)** — Shared Brain architecture
- **[ADR-062](../../docs/adr/ADR-062-brainpedia-architecture.md)** — Brainpedia
- **[ADR-063](../../docs/adr/ADR-063-wasm-executable-nodes.md)** — WASM nodes
- **[ADR-076](../../docs/adr/ADR-076-agi-capability-wiring-architecture.md)** — AGI capability wiring
- **[ADR-077](../../docs/adr/ADR-077-midstream-brain-integration.md)** — Midstream integration
## License
MIT

View file

@ -0,0 +1,335 @@
//! HTTPS client to the Cloud Run brain backend
use crate::embed::{generate_embedding, LoraWeights};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ClientError {
#[error("HTTP error: {0}")]
Http(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Server error ({status}): {message}")]
Server { status: u16, message: String },
}
/// Client for the brain.ruv.io backend
pub struct BrainClient {
base_url: String,
api_key: String,
http: reqwest::Client,
}
impl BrainClient {
pub fn new() -> Self {
let base_url = std::env::var("BRAIN_URL")
.unwrap_or_else(|_| "https://ruvbrain-875130704813.us-central1.run.app".to_string());
let api_key = std::env::var("BRAIN_API_KEY")
.unwrap_or_else(|_| "anonymous".to_string());
Self {
base_url,
api_key,
http: reqwest::Client::new(),
}
}
pub fn with_url(url: String) -> Self {
let api_key = std::env::var("BRAIN_API_KEY")
.unwrap_or_else(|_| "anonymous".to_string());
Self {
base_url: url,
api_key,
http: reqwest::Client::new(),
}
}
/// Share a memory
pub async fn share(
&self,
category: &str,
title: &str,
content: &str,
tags: &[String],
code_snippet: Option<&str>,
) -> Result<serde_json::Value, ClientError> {
let body = serde_json::json!({
"category": category,
"title": title,
"content": content,
"tags": tags,
"code_snippet": code_snippet,
"embedding": generate_embedding(content),
"witness_hash": hex::encode(sha3_hash(content.as_bytes())),
});
self.post("/v1/memories", &body).await
}
/// Search memories
pub async fn search(
&self,
query: &str,
category: Option<&str>,
tags: Option<&str>,
limit: Option<usize>,
min_quality: Option<f64>,
) -> Result<serde_json::Value, ClientError> {
let mut params = vec![("q", query.to_string())];
if let Some(c) = category { params.push(("category", c.to_string())); }
if let Some(t) = tags { params.push(("tags", t.to_string())); }
if let Some(l) = limit { params.push(("limit", l.to_string())); }
if let Some(q) = min_quality { params.push(("min_quality", q.to_string())); }
self.get_with_params("/v1/memories/search", &params).await
}
/// Get a memory by ID
pub async fn get(&self, id: &str) -> Result<serde_json::Value, ClientError> {
self.get_path(&format!("/v1/memories/{id}")).await
}
/// Vote on a memory
pub async fn vote(&self, id: &str, direction: &str) -> Result<serde_json::Value, ClientError> {
let body = serde_json::json!({ "direction": direction });
self.post(&format!("/v1/memories/{id}/vote"), &body).await
}
/// Transfer knowledge between domains
pub async fn transfer(&self, source: &str, target: &str) -> Result<serde_json::Value, ClientError> {
let body = serde_json::json!({
"source_domain": source,
"target_domain": target,
});
self.post("/v1/transfer", &body).await
}
/// Get drift report
pub async fn drift(&self, domain: Option<&str>, since: Option<&str>) -> Result<serde_json::Value, ClientError> {
let mut params = Vec::new();
if let Some(d) = domain { params.push(("domain", d.to_string())); }
if let Some(s) = since { params.push(("since", s.to_string())); }
self.get_with_params("/v1/drift", &params).await
}
/// Get partition topology
pub async fn partition(&self, domain: Option<&str>, min_size: Option<usize>) -> Result<serde_json::Value, ClientError> {
let mut params = Vec::new();
if let Some(d) = domain { params.push(("domain", d.to_string())); }
if let Some(s) = min_size { params.push(("min_cluster_size", s.to_string())); }
self.get_with_params("/v1/partition", &params).await
}
/// List memories
pub async fn list(&self, category: Option<&str>, limit: Option<usize>) -> Result<serde_json::Value, ClientError> {
let mut params = Vec::new();
if let Some(c) = category { params.push(("category", c.to_string())); }
if let Some(l) = limit { params.push(("limit", l.to_string())); }
self.get_with_params("/v1/memories/list", &params).await
}
/// Delete a memory
pub async fn delete(&self, id: &str) -> Result<(), ClientError> {
let url = format!("{}/v1/memories/{id}", self.base_url);
let resp = self.http
.delete(&url)
.bearer_auth(&self.api_key)
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status().as_u16();
let msg = resp.text().await.unwrap_or_default();
Err(ClientError::Server { status, message: msg })
}
}
/// Get system status
pub async fn status(&self) -> Result<serde_json::Value, ClientError> {
self.get_path("/v1/status").await
}
/// Get latest consensus LoRA weights from server
pub async fn lora_latest(&self) -> Result<Option<LoraWeights>, ClientError> {
let result = self.get_path("/v1/lora/latest").await;
match result {
Ok(val) => {
// Server returns {"weights": null} if no consensus yet
if val.get("weights").map_or(true, |w| w.is_null()) {
return Ok(None);
}
let weights: LoraWeights = serde_json::from_value(
val.get("weights").cloned().unwrap_or_default()
).map_err(|e| ClientError::Serialization(e.to_string()))?;
Ok(Some(weights))
}
Err(ClientError::Server { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
/// Submit local LoRA weights for federated aggregation
pub async fn lora_submit(&self, weights: &LoraWeights) -> Result<serde_json::Value, ClientError> {
let body = serde_json::to_value(weights)
.map_err(|e| ClientError::Serialization(e.to_string()))?;
self.post("/v1/lora/submit", &body).await
}
// ---- Brainpedia (ADR-062) ----
/// Create a Brainpedia page
pub async fn create_page(&self, body: &serde_json::Value) -> Result<serde_json::Value, ClientError> {
self.post("/v1/pages", body).await
}
/// Get a Brainpedia page with its delta log and evidence
pub async fn get_page(&self, id: &str) -> Result<serde_json::Value, ClientError> {
self.get_path(&format!("/v1/pages/{id}")).await
}
/// Submit a delta to a page
pub async fn submit_delta(&self, page_id: &str, body: &serde_json::Value) -> Result<serde_json::Value, ClientError> {
self.post(&format!("/v1/pages/{page_id}/deltas"), body).await
}
/// List deltas for a page
pub async fn list_deltas(&self, page_id: &str) -> Result<serde_json::Value, ClientError> {
self.get_path(&format!("/v1/pages/{page_id}/deltas")).await
}
/// Add evidence to a page
pub async fn add_evidence(&self, page_id: &str, body: &serde_json::Value) -> Result<serde_json::Value, ClientError> {
self.post(&format!("/v1/pages/{page_id}/evidence"), body).await
}
/// Promote a page from Draft to Canonical
pub async fn promote_page(&self, page_id: &str) -> Result<serde_json::Value, ClientError> {
self.post(&format!("/v1/pages/{page_id}/promote"), &serde_json::json!({})).await
}
// ---- WASM Executable Nodes (ADR-063) ----
/// List all published WASM nodes
pub async fn list_nodes(&self) -> Result<serde_json::Value, ClientError> {
self.get_path("/v1/nodes").await
}
/// Publish a WASM node
pub async fn publish_node(&self, body: &serde_json::Value) -> Result<serde_json::Value, ClientError> {
self.post("/v1/nodes", body).await
}
/// Get WASM node metadata
pub async fn get_node(&self, id: &str) -> Result<serde_json::Value, ClientError> {
self.get_path(&format!("/v1/nodes/{id}")).await
}
/// Download WASM binary
pub async fn get_node_wasm(&self, id: &str) -> Result<Vec<u8>, ClientError> {
let url = format!("{}/v1/nodes/{id}.wasm", self.base_url);
let resp = self.http
.get(&url)
.bearer_auth(&self.api_key)
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
if resp.status().is_success() {
resp.bytes().await
.map(|b| b.to_vec())
.map_err(|e| ClientError::Http(e.to_string()))
} else {
let status = resp.status().as_u16();
let msg = resp.text().await.unwrap_or_default();
Err(ClientError::Server { status, message: msg })
}
}
/// Revoke a WASM node
pub async fn revoke_node(&self, id: &str) -> Result<(), ClientError> {
let url = format!("{}/v1/nodes/{id}/revoke", self.base_url);
let resp = self.http
.post(&url)
.bearer_auth(&self.api_key)
.json(&serde_json::json!({}))
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
if resp.status().is_success() || resp.status().as_u16() == 204 {
Ok(())
} else {
let status = resp.status().as_u16();
let msg = resp.text().await.unwrap_or_default();
Err(ClientError::Server { status, message: msg })
}
}
// ---- HTTP helpers ----
async fn get_path(&self, path: &str) -> Result<serde_json::Value, ClientError> {
let url = format!("{}{path}", self.base_url);
let resp = self.http
.get(&url)
.bearer_auth(&self.api_key)
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
self.handle_response(resp).await
}
async fn get_with_params(&self, path: &str, params: &[(&str, String)]) -> Result<serde_json::Value, ClientError> {
let url = format!("{}{path}", self.base_url);
let resp = self.http
.get(&url)
.bearer_auth(&self.api_key)
.query(params)
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
self.handle_response(resp).await
}
async fn post(&self, path: &str, body: &serde_json::Value) -> Result<serde_json::Value, ClientError> {
let url = format!("{}{path}", self.base_url);
let resp = self.http
.post(&url)
.bearer_auth(&self.api_key)
.json(body)
.send()
.await
.map_err(|e| ClientError::Http(e.to_string()))?;
self.handle_response(resp).await
}
async fn handle_response(&self, resp: reqwest::Response) -> Result<serde_json::Value, ClientError> {
let status = resp.status().as_u16();
if status >= 400 {
let msg = resp.text().await.unwrap_or_default();
return Err(ClientError::Server { status, message: msg });
}
resp.json().await.map_err(|e| ClientError::Serialization(e.to_string()))
}
}
impl Default for BrainClient {
fn default() -> Self {
Self::new()
}
}
/// SHAKE-256 hash
fn sha3_hash(data: &[u8]) -> [u8; 32] {
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
let mut hasher = Shake256::default();
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut buf = [0u8; 32];
reader.read(&mut buf);
buf
}

View file

@ -0,0 +1,579 @@
//! Embedding generation for brain memories
//!
//! Two-stage embedding pipeline:
//! 1. **Structured Hash Features** (deterministic, identical across all sessions):
//! Multi-granularity hashing — unigram, bigram, trigram tokens hashed into
//! disjoint subspaces with signed hashing to reduce collision bias.
//! 2. **MicroLoRA Transform** (learned, federated across sessions):
//! Rank-2 LoRA adapter applied to the frozen hash features. Weights are
//! learned locally via SONA and periodically federated to/from the server.
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
use sona::{SonaEngine, LearnedPattern};
use sona::engine::SonaEngineBuilder;
use std::sync::{Arc, Mutex};
/// Embedding dimension (128 f32s = 512 bytes)
pub const EMBEDDING_DIM: usize = 128;
/// Subspace allocation for multi-granularity hashing:
/// - Unigram: dims [0..42) = 42 dims (33%)
/// - Bigram: dims [42..84) = 42 dims (33%)
/// - Trigram: dims [84..128) = 44 dims (34%)
const UNIGRAM_START: usize = 0;
const UNIGRAM_END: usize = 42;
const BIGRAM_START: usize = 42;
const BIGRAM_END: usize = 84;
const TRIGRAM_START: usize = 84;
const TRIGRAM_END: usize = EMBEDDING_DIM; // 128
/// LoRA weights exported for federation
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LoraWeights {
/// Down projection weights (hidden_dim * rank)
pub down_proj: Vec<f32>,
/// Up projection weights (rank * hidden_dim)
pub up_proj: Vec<f32>,
/// LoRA rank
pub rank: usize,
/// Hidden dimension
pub hidden_dim: usize,
/// Number of local training steps that produced these weights
pub evidence_count: u64,
}
impl LoraWeights {
/// Validate weight shapes and values (Gate A: policy validity)
pub fn validate(&self) -> Result<(), String> {
// Shape check
let expected_down = self.hidden_dim * self.rank;
let expected_up = self.rank * self.hidden_dim;
if self.down_proj.len() != expected_down {
return Err(format!(
"down_proj shape mismatch: expected {expected_down}, got {}",
self.down_proj.len()
));
}
if self.up_proj.len() != expected_up {
return Err(format!(
"up_proj shape mismatch: expected {expected_up}, got {}",
self.up_proj.len()
));
}
// NaN/Inf check
for (i, &v) in self.down_proj.iter().chain(self.up_proj.iter()).enumerate() {
if v.is_nan() || v.is_infinite() {
return Err(format!("NaN/Inf at index {i}"));
}
}
// Norm check: reject if L2 norm of either projection > 100
let down_norm: f32 = self.down_proj.iter().map(|x| x * x).sum::<f32>().sqrt();
let up_norm: f32 = self.up_proj.iter().map(|x| x * x).sum::<f32>().sqrt();
if down_norm > 100.0 || up_norm > 100.0 {
return Err(format!("Weight norm too large: down={down_norm:.1}, up={up_norm:.1}"));
}
// Minimum evidence
if self.evidence_count < 5 {
return Err(format!(
"Insufficient evidence: {} (minimum 5)",
self.evidence_count
));
}
Ok(())
}
/// Clip weights to [-2, 2] range
pub fn clip(&mut self) {
for v in self.down_proj.iter_mut().chain(self.up_proj.iter_mut()) {
*v = v.clamp(-2.0, 2.0);
}
}
/// Compute L2 distance to another set of weights
pub fn l2_distance(&self, other: &LoraWeights) -> f32 {
let d: f32 = self.down_proj.iter().zip(other.down_proj.iter())
.chain(self.up_proj.iter().zip(other.up_proj.iter()))
.map(|(a, b)| (a - b).powi(2))
.sum();
d.sqrt()
}
}
/// Brain embedding engine wrapping SONA with structured hash + MicroLoRA
pub struct BrainEmbedder {
engine: Option<Arc<Mutex<SonaEngine>>>,
/// Locally cached consensus weights from the server (applied on embed)
consensus_lora: Option<LoraWeights>,
/// Count of embeddings processed (used as evidence_count for export)
embed_count: u64,
}
impl BrainEmbedder {
/// Create with real SONA engine
pub fn new() -> Self {
let engine = match std::panic::catch_unwind(|| {
SonaEngineBuilder::new()
.hidden_dim(EMBEDDING_DIM)
.micro_lora_rank(2)
.pattern_clusters(50)
.quality_threshold(0.3)
.build()
}) {
Ok(e) => Some(Arc::new(Mutex::new(e))),
Err(_) => None,
};
Self {
engine,
consensus_lora: None,
embed_count: 0,
}
}
/// Create with hash-only fallback (no SONA)
pub fn hash_only() -> Self {
Self {
engine: None,
consensus_lora: None,
embed_count: 0,
}
}
/// Generate embedding for text content.
///
/// Pipeline: text -> structured hash features -> MicroLoRA transform -> L2 normalize
pub fn embed(&mut self, text: &str) -> Vec<f32> {
self.embed_count += 1;
// Stage 1: Deterministic structured hash features
let hash_features = generate_structured_hash_features(text);
// Stage 2: Apply MicroLoRA transform (consensus weights if available, then SONA)
let transformed = self.apply_lora_transform(&hash_features);
// Feed into SONA for trajectory learning if available
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
let builder = eng.begin_trajectory(transformed.clone());
eng.end_trajectory(builder, 0.5);
// Check for refined patterns
let patterns = eng.find_patterns(&transformed, 1);
if let Some(pattern) = patterns.first() {
if pattern.similarity(&transformed) > 0.3 {
return normalize_l2(&pattern.centroid);
}
}
}
}
transformed
}
/// Apply MicroLoRA transform to hash features
fn apply_lora_transform(&self, features: &[f32]) -> Vec<f32> {
// Try consensus weights first (from server federation)
if let Some(ref lora) = self.consensus_lora {
return apply_lora_forward(features, lora);
}
// Try SONA engine's local MicroLoRA
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
let lora_state = eng.export_lora_state();
if let Some(layer) = lora_state.micro_lora_layers.first() {
let local_lora = LoraWeights {
down_proj: layer.lora_a.clone(),
up_proj: layer.lora_b.clone(),
rank: layer.rank,
hidden_dim: layer.input_dim,
evidence_count: self.embed_count,
};
return apply_lora_forward(features, &local_lora);
}
}
}
// No LoRA available — return raw hash features
normalize_l2(features)
}
/// Import consensus LoRA weights from the server
pub fn import_consensus_weights(&mut self, weights: LoraWeights) {
self.consensus_lora = Some(weights);
}
/// Export local MicroLoRA weights for federation
pub fn export_local_weights(&self) -> Option<LoraWeights> {
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
let lora_state = eng.export_lora_state();
if let Some(layer) = lora_state.micro_lora_layers.first() {
return Some(LoraWeights {
down_proj: layer.lora_a.clone(),
up_proj: layer.lora_b.clone(),
rank: layer.rank,
hidden_dim: layer.input_dim,
evidence_count: self.embed_count,
});
}
}
}
None
}
/// Get number of embeddings processed
pub fn embed_count(&self) -> u64 {
self.embed_count
}
/// Record quality feedback for a trajectory
pub fn record_feedback(&self, embedding: &[f32], quality: f32) {
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
let builder = eng.begin_trajectory(embedding.to_vec());
eng.end_trajectory(builder, quality);
}
}
}
/// Find similar patterns from SONA's learned bank
pub fn find_similar(&self, query: &[f32], k: usize) -> Vec<LearnedPattern> {
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
return eng.find_patterns(query, k);
}
}
vec![]
}
/// Force a learning cycle
pub fn force_learn(&self) -> Option<String> {
if let Some(ref engine) = self.engine {
if let Ok(eng) = engine.lock() {
return Some(eng.force_learn());
}
}
None
}
/// Check if SONA engine is active
pub fn has_sona(&self) -> bool {
self.engine.is_some()
}
/// Check if consensus LoRA weights are loaded
pub fn has_consensus_lora(&self) -> bool {
self.consensus_lora.is_some()
}
}
impl Default for BrainEmbedder {
fn default() -> Self {
Self::new()
}
}
/// Apply LoRA forward pass: output = L2_normalize(input + scale * (input @ down) @ up)
fn apply_lora_forward(input: &[f32], lora: &LoraWeights) -> Vec<f32> {
let dim = lora.hidden_dim;
let rank = lora.rank;
let scale = (rank as f32).recip(); // alpha/rank = 1.0
// down_proj: input (dim,) @ down (dim, rank) -> intermediate (rank,)
let mut intermediate = vec![0.0f32; rank];
for r in 0..rank {
for d in 0..dim.min(input.len()) {
intermediate[r] += input[d] * lora.down_proj[d * rank + r];
}
}
// up_proj: intermediate (rank,) @ up (rank, dim) -> delta (dim,)
let mut output: Vec<f32> = input.to_vec();
output.resize(dim, 0.0);
for d in 0..dim {
let mut delta = 0.0f32;
for r in 0..rank {
delta += intermediate[r] * lora.up_proj[r * dim + d];
}
output[d] += scale * delta;
}
normalize_l2(&output)
}
/// L2 normalize a vector
pub fn normalize_l2(v: &[f32]) -> Vec<f32> {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
v.iter().map(|x| x / norm).collect()
} else {
v.to_vec()
}
}
/// Generate structured multi-granularity hash features.
///
/// Splits text into unigram, bigram, and trigram tokens. Each n-gram level
/// hashes into a disjoint subspace of the embedding vector using signed hashing
/// (hash determines both the bucket index AND the sign, reducing collision bias).
///
/// This is deterministic and identical across all sessions — the frozen base
/// that MicroLoRA adapts on top of.
pub fn generate_structured_hash_features(text: &str) -> Vec<f32> {
let mut features = vec![0.0f32; EMBEDDING_DIM];
let lower = text.to_lowercase();
let words: Vec<&str> = lower.split_whitespace().collect();
// Unigram features: each word hashes into dims [0..42)
let unigram_dim = UNIGRAM_END - UNIGRAM_START;
for word in &words {
let (bucket, sign) = signed_hash(word.as_bytes(), b"uni", unigram_dim);
features[UNIGRAM_START + bucket] += sign;
}
// Bigram features: consecutive word pairs hash into dims [42..84)
let bigram_dim = BIGRAM_END - BIGRAM_START;
for pair in words.windows(2) {
let key = format!("{} {}", pair[0], pair[1]);
let (bucket, sign) = signed_hash(key.as_bytes(), b"bi", bigram_dim);
features[BIGRAM_START + bucket] += sign;
}
// Trigram features: consecutive word triples hash into dims [84..128)
let trigram_dim = TRIGRAM_END - TRIGRAM_START;
for triple in words.windows(3) {
let key = format!("{} {} {}", triple[0], triple[1], triple[2]);
let (bucket, sign) = signed_hash(key.as_bytes(), b"tri", trigram_dim);
features[TRIGRAM_START + bucket] += sign;
}
// Also add character n-gram features for short texts or single words
if words.len() <= 2 {
let chars: Vec<char> = lower.chars().filter(|c| c.is_alphanumeric()).collect();
// Character trigrams into the trigram subspace
for window in chars.windows(3) {
let key: String = window.iter().collect();
let (bucket, sign) = signed_hash(key.as_bytes(), b"ctri", trigram_dim);
features[TRIGRAM_START + bucket] += sign * 0.5; // Lower weight for char ngrams
}
}
// L2 normalize and clip to [-1, 1]
let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
for v in &mut features {
*v = (*v / norm).clamp(-1.0, 1.0);
}
}
features
}
/// Signed hash: returns (bucket_index, +1.0 or -1.0).
/// Uses SHAKE-256 for uniform distribution. The first 4 bytes determine the bucket,
/// the 5th byte determines the sign.
fn signed_hash(data: &[u8], salt: &[u8], num_buckets: usize) -> (usize, f32) {
let mut hasher = Shake256::default();
hasher.update(b"ruvector-shf:");
hasher.update(salt);
hasher.update(b":");
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut buf = [0u8; 5];
reader.read(&mut buf);
let bucket = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize % num_buckets;
let sign = if buf[4] & 1 == 0 { 1.0f32 } else { -1.0f32 };
(bucket, sign)
}
/// Public convenience: generate an embedding using structured hash features only.
/// Callers needing SONA/LoRA should use BrainEmbedder directly.
pub fn generate_embedding(text: &str) -> Vec<f32> {
generate_structured_hash_features(text)
}
/// Compute cosine similarity between two embeddings
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a < 1e-10 || norm_b < 1e-10 {
return 0.0;
}
dot / (norm_a * norm_b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedding_deterministic() {
let e1 = generate_embedding("hello world");
let e2 = generate_embedding("hello world");
assert_eq!(e1, e2);
}
#[test]
fn test_embedding_dimension() {
let e = generate_embedding("test");
assert_eq!(e.len(), EMBEDDING_DIM);
}
#[test]
fn test_embedding_normalized() {
let e = generate_embedding("a longer text for normalization testing");
let norm: f32 = e.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01, "norm was {norm}");
}
#[test]
fn test_similar_texts_closer() {
// Structured hash features should give higher similarity for
// texts sharing many n-grams than completely unrelated texts
let e1 = generate_embedding("rust programming language features");
let e2 = generate_embedding("rust programming language syntax");
let e3 = generate_embedding("cooking recipes for dinner tonight");
let sim12 = cosine_similarity(&e1, &e2);
let sim13 = cosine_similarity(&e1, &e3);
// Texts sharing 3/4 words should be more similar than disjoint texts
assert!(
sim12 > sim13,
"similar texts should be closer: sim12={sim12}, sim13={sim13}"
);
}
#[test]
fn test_disjoint_subspaces() {
// Verify that a single word only activates the unigram subspace
let e = generate_structured_hash_features("hello");
// Unigram subspace should have non-zero values
let uni_energy: f32 = e[UNIGRAM_START..UNIGRAM_END].iter().map(|x| x * x).sum();
assert!(uni_energy > 0.0, "unigram subspace should be active");
// Bigram/trigram subspaces should be zero (single word = no pairs/triples)
// But char trigrams may activate trigram subspace for short texts
}
#[test]
fn test_signed_hash_distribution() {
// Verify signed_hash produces both positive and negative signs
let mut pos = 0;
let mut neg = 0;
for i in 0..100 {
let key = format!("test-{i}");
let (_, sign) = signed_hash(key.as_bytes(), b"test", 42);
if sign > 0.0 { pos += 1; } else { neg += 1; }
}
// Both signs should appear (probabilistic, but 100 trials is enough)
assert!(pos > 10 && neg > 10, "pos={pos}, neg={neg}");
}
#[test]
fn test_brain_embedder_creates() {
let embedder = BrainEmbedder::new();
let _ = embedder.has_sona();
}
#[test]
fn test_brain_embedder_embed() {
let mut embedder = BrainEmbedder::new();
let emb = embedder.embed("hello world");
assert_eq!(emb.len(), EMBEDDING_DIM);
let norm: f32 = emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.05, "norm was {norm}");
}
#[test]
fn test_hash_fallback() {
let mut embedder = BrainEmbedder::hash_only();
assert!(!embedder.has_sona());
assert!(!embedder.has_consensus_lora());
let emb = embedder.embed("hello world");
assert_eq!(emb.len(), EMBEDDING_DIM);
// Hash-only should match generate_embedding (both L2-normalized structured hash)
let direct = generate_embedding("hello world");
assert_eq!(emb, direct);
}
#[test]
fn test_lora_weights_validate() {
let valid = LoraWeights {
down_proj: vec![0.1; 256],
up_proj: vec![0.1; 256],
rank: 2,
hidden_dim: 128,
evidence_count: 10,
};
assert!(valid.validate().is_ok());
// Bad shape
let bad_shape = LoraWeights {
down_proj: vec![0.1; 100],
up_proj: vec![0.1; 256],
rank: 2,
hidden_dim: 128,
evidence_count: 10,
};
assert!(bad_shape.validate().is_err());
// NaN
let mut nan_weights = valid.clone();
nan_weights.down_proj[0] = f32::NAN;
assert!(nan_weights.validate().is_err());
// Low evidence
let low_ev = LoraWeights {
evidence_count: 2,
..valid.clone()
};
assert!(low_ev.validate().is_err());
}
#[test]
fn test_lora_forward_pass() {
// Zero LoRA should return normalized input unchanged
let input = generate_structured_hash_features("test input");
let zero_lora = LoraWeights {
down_proj: vec![0.0; 256],
up_proj: vec![0.0; 256],
rank: 2,
hidden_dim: 128,
evidence_count: 10,
};
let output = apply_lora_forward(&input, &zero_lora);
let expected = normalize_l2(&input);
for (a, b) in output.iter().zip(expected.iter()) {
assert!((a - b).abs() < 1e-6, "zero LoRA should be identity");
}
}
#[test]
fn test_consensus_import() {
let mut embedder = BrainEmbedder::hash_only();
assert!(!embedder.has_consensus_lora());
let weights = LoraWeights {
down_proj: vec![0.01; 256],
up_proj: vec![0.01; 256],
rank: 2,
hidden_dim: 128,
evidence_count: 100,
};
embedder.import_consensus_weights(weights);
assert!(embedder.has_consensus_lora());
// Embedding should now go through LoRA transform
let emb = embedder.embed("test with lora");
assert_eq!(emb.len(), EMBEDDING_DIM);
let norm: f32 = emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.05, "norm was {norm}");
}
#[test]
fn test_find_similar_empty() {
let embedder = BrainEmbedder::hash_only();
let results = embedder.find_similar(&[0.0; EMBEDDING_DIM], 5);
assert!(results.is_empty());
}
}

View file

@ -0,0 +1,39 @@
//! mcp-brain: MCP server for the RuVector Shared Brain
//!
//! Enables Claude Code sessions to share and discover learning across sessions.
//! Knowledge is stored as RVF cognitive containers with witness chains,
//! Ed25519 signatures, and differential privacy proofs.
//!
//! # MCP Tools (10)
//!
//! - **brain_share**: Share a learning with the collective
//! - **brain_search**: Semantic search across shared knowledge
//! - **brain_get**: Retrieve a specific memory with full provenance
//! - **brain_vote**: Quality-gate a memory (Bayesian update)
//! - **brain_transfer**: Apply learned priors cross-domain
//! - **brain_drift**: Check if shared knowledge has drifted
//! - **brain_partition**: Get knowledge partitioned by mincut topology
//! - **brain_list**: List recent memories by category/quality
//! - **brain_delete**: Delete own contribution
//! - **brain_status**: System health
//!
//! # Usage
//!
//! ```no_run
//! use mcp_brain::McpBrainServer;
//!
//! #[tokio::main]
//! async fn main() {
//! let server = McpBrainServer::new();
//! server.run_stdio().await.expect("Server failed");
//! }
//! ```
pub mod client;
pub mod embed;
pub mod pipeline;
pub mod server;
pub mod tools;
pub mod types;
pub use server::McpBrainServer;

View file

@ -0,0 +1,25 @@
//! MCP Brain server binary
//!
//! Runs the MCP Brain server on stdio for integration with Claude Code.
use mcp_brain::McpBrainServer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(fmt::layer().with_writer(std::io::stderr))
.with(filter)
.init();
let server = McpBrainServer::new();
tracing::info!("MCP Brain server v{} starting", env!("CARGO_PKG_VERSION"));
tracing::info!("Backend: brain.ruv.io");
server.run_stdio().await?;
Ok(())
}

View file

@ -0,0 +1,355 @@
//! Local processing pipeline: PII -> embed -> sign
use regex_lite::Regex;
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
/// Pipeline for processing knowledge before sharing.
/// Pre-compiles 12 PII regex patterns for efficient reuse.
pub struct BrainPipeline {
pii_patterns: Vec<(Regex, &'static str)>,
}
impl BrainPipeline {
pub fn new() -> Self {
let patterns = vec![
// 1. Unix home paths
(Regex::new(r"/(?:home|Users|root)/[^\s]+").unwrap(), "[REDACTED_PATH]"),
// 2. Windows paths
(Regex::new(r"C:\\Users\\[^\s]+").unwrap(), "[REDACTED_PATH]"),
// 3. API keys: sk-..., ghp_..., gho_..., xoxb-..., xoxp-..., AKIA...
(Regex::new(r"(?:sk-|ghp_|gho_|xoxb-|xoxp-|AKIA)[A-Za-z0-9_/-]+").unwrap(), "[REDACTED_KEY]"),
// 4. Bearer tokens
(Regex::new(r"Bearer\s+[A-Za-z0-9_./-]+").unwrap(), "[REDACTED_TOKEN]"),
// 5. IPv4 addresses
(Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap(), "[REDACTED_IP]"),
// 6. Email addresses
(Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b").unwrap(), "[REDACTED_EMAIL]"),
// 7. SSH keys
(Regex::new(r"ssh-(?:rsa|ed25519|ecdsa)\s+[A-Za-z0-9+/=]+").unwrap(), "[REDACTED_SSH_KEY]"),
// 8. JWT tokens
(Regex::new(r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+").unwrap(), "[REDACTED_JWT]"),
// 9. Private keys in PEM
(Regex::new(r"-----BEGIN[A-Z ]+PRIVATE KEY-----").unwrap(), "[REDACTED_PRIVATE_KEY]"),
// 10. Secret/password/token assignments
(Regex::new(r"(?i)(?:secret|password|passwd|token|key)\s*[:=]\s*['\x22]?[A-Za-z0-9+/=_-]{16,}").unwrap(), "[REDACTED_SECRET]"),
// 11. API key / access token credentials
(Regex::new(r"(?i)(?:api[_-]?key|access[_-]?token)\s*[:=]\s*['\x22]?[a-f0-9-]{32,}").unwrap(), "[REDACTED_CREDENTIAL]"),
// 12. Internal hostnames
(Regex::new(r"\b(?:localhost|127\.0\.0\.1|0\.0\.0\.0|internal\.[a-z.]+)\b").unwrap(), "[REDACTED_HOST]"),
];
Self { pii_patterns: patterns }
}
/// Strip PII from text using all 12 pattern categories
pub fn strip_pii(&self, text: &str) -> String {
let mut result = text.to_string();
for (pattern, replacement) in &self.pii_patterns {
result = pattern.replace_all(&result, *replacement).to_string();
}
result
}
/// Check if text contains PII (any pattern matches)
pub fn contains_pii(&self, text: &str) -> bool {
self.pii_patterns.iter().any(|(pat, _)| pat.is_match(text))
}
/// Build a linked witness chain from a list of operations
pub fn build_witness_chain(operations: &[&str]) -> WitnessChain {
let mut chain = WitnessChain::new();
for op in operations {
chain.append(op);
}
chain
}
}
impl Default for BrainPipeline {
fn default() -> Self {
Self::new()
}
}
/// A single witness entry in the chain
#[derive(Debug, Clone)]
pub struct WitnessEntry {
pub action: String,
pub hash: [u8; 32],
pub timestamp_ns: u64,
}
/// Linked chain of witness entries using SHAKE-256.
/// Each entry's hash = SHAKE256(prev_hash || action || timestamp).
pub struct WitnessChain {
entries: Vec<WitnessEntry>,
prev_hash: [u8; 32],
}
impl WitnessChain {
pub fn new() -> Self {
Self {
entries: Vec::new(),
prev_hash: [0u8; 32],
}
}
/// Append an action to the witness chain
pub fn append(&mut self, action: &str) -> &WitnessEntry {
let timestamp_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let mut hasher = Shake256::default();
hasher.update(&self.prev_hash);
hasher.update(action.as_bytes());
hasher.update(&timestamp_ns.to_le_bytes());
let mut reader = hasher.finalize_xof();
let mut hash = [0u8; 32];
reader.read(&mut hash);
let entry = WitnessEntry {
action: action.to_string(),
hash,
timestamp_ns,
};
self.prev_hash = hash;
self.entries.push(entry);
self.entries.last().unwrap()
}
/// Get the final witness hash as hex string
pub fn finalize(&self) -> String {
hex::encode(self.prev_hash)
}
/// Verify chain integrity by recomputing each hash
pub fn verify(&self) -> bool {
let mut prev = [0u8; 32];
for entry in &self.entries {
let mut hasher = Shake256::default();
hasher.update(&prev);
hasher.update(entry.action.as_bytes());
hasher.update(&entry.timestamp_ns.to_le_bytes());
let mut reader = hasher.finalize_xof();
let mut expected = [0u8; 32];
reader.read(&mut expected);
if expected != entry.hash {
return false;
}
prev = entry.hash;
}
true
}
/// Get number of entries
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for WitnessChain {
fn default() -> Self {
Self::new()
}
}
/// Generate a witness hash for an operation chain (backward compat)
pub fn witness_hash(operations: &[&str]) -> String {
let mut chain = WitnessChain::new();
for op in operations {
chain.append(op);
}
chain.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_pii_unix_paths() {
let pipeline = BrainPipeline::new();
let input = "Found at /home/alice/secrets.txt";
let output = pipeline.strip_pii(input);
assert!(!output.contains("/home/alice"));
assert!(output.contains("[REDACTED_PATH]"));
}
#[test]
fn test_strip_pii_windows_paths() {
let pipeline = BrainPipeline::new();
let input = r"Found at C:\Users\bob\documents\secret.txt";
let output = pipeline.strip_pii(input);
assert!(!output.contains(r"C:\Users\bob"));
assert!(output.contains("[REDACTED_PATH]"));
}
#[test]
fn test_strip_pii_api_keys() {
let pipeline = BrainPipeline::new();
let input = "Using key sk-abc123xyz and ghp_TokenValue123";
let output = pipeline.strip_pii(input);
assert!(!output.contains("sk-abc123xyz"));
assert!(!output.contains("ghp_TokenValue123"));
assert!(output.contains("[REDACTED_KEY]"));
}
#[test]
fn test_strip_pii_bearer_token() {
let pipeline = BrainPipeline::new();
let input = "Header: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig";
let output = pipeline.strip_pii(input);
assert!(!output.contains("eyJhbGciOiJSUzI1NiJ9"));
assert!(output.contains("[REDACTED_TOKEN]"));
}
#[test]
fn test_strip_pii_ip_address() {
let pipeline = BrainPipeline::new();
let input = "Server at 192.168.1.100 is running";
let output = pipeline.strip_pii(input);
assert!(!output.contains("192.168.1.100"));
assert!(output.contains("[REDACTED_IP]"));
}
#[test]
fn test_strip_pii_email() {
let pipeline = BrainPipeline::new();
let input = "Contact user@example.com for help";
let output = pipeline.strip_pii(input);
assert!(!output.contains("user@example.com"));
assert!(output.contains("[REDACTED_EMAIL]"));
}
#[test]
fn test_strip_pii_ssh_key() {
let pipeline = BrainPipeline::new();
let input = "Key: ssh-rsa AAAAB3NzaC1yc2EAAA";
let output = pipeline.strip_pii(input);
assert!(!output.contains("ssh-rsa AAAA"));
assert!(output.contains("[REDACTED_SSH_KEY]"));
}
#[test]
fn test_strip_pii_jwt() {
let pipeline = BrainPipeline::new();
let input = "Token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123def456";
let output = pipeline.strip_pii(input);
assert!(!output.contains("eyJhbGciOiJIUzI1NiJ9"));
assert!(output.contains("[REDACTED_JWT]"));
}
#[test]
fn test_strip_pii_private_key() {
let pipeline = BrainPipeline::new();
let input = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpA...\n-----END RSA PRIVATE KEY-----";
let output = pipeline.strip_pii(input);
assert!(!output.contains("-----BEGIN RSA PRIVATE KEY-----"));
assert!(output.contains("[REDACTED_PRIVATE_KEY]"));
}
#[test]
fn test_strip_pii_secret_assignment() {
let pipeline = BrainPipeline::new();
let input = "secret=abcdefghij1234567890abcdefghij12";
let output = pipeline.strip_pii(input);
assert!(output.contains("[REDACTED_SECRET]"));
}
#[test]
fn test_strip_pii_localhost() {
let pipeline = BrainPipeline::new();
let input = "Connect to localhost for debugging";
let output = pipeline.strip_pii(input);
assert!(!output.contains("localhost"));
assert!(output.contains("[REDACTED_HOST]"));
}
#[test]
fn test_contains_pii_detects_patterns() {
let pipeline = BrainPipeline::new();
assert!(pipeline.contains_pii("path /home/user/.ssh"));
assert!(pipeline.contains_pii("token sk-abc123"));
assert!(pipeline.contains_pii("email user@host.com"));
assert!(pipeline.contains_pii("server at 10.0.0.1 port 80"));
assert!(!pipeline.contains_pii("this is clean text with no PII"));
}
#[test]
fn test_contains_pii_clean_after_strip() {
let pipeline = BrainPipeline::new();
let dirty = "Send to user@example.com at /home/alice/docs from 192.168.1.1";
let clean = pipeline.strip_pii(dirty);
assert!(!pipeline.contains_pii(&clean));
}
#[test]
fn test_witness_chain_integrity() {
let mut chain = WitnessChain::new();
chain.append("step_1");
chain.append("step_2");
chain.append("step_3");
chain.append("step_4");
chain.append("step_5");
assert_eq!(chain.len(), 5);
assert!(chain.verify());
}
#[test]
fn test_witness_chain_tamper() {
let mut chain = WitnessChain::new();
chain.append("step_1");
chain.append("step_2");
chain.append("step_3");
// Tamper with the second entry's hash
chain.entries[1].hash[0] ^= 0xFF;
assert!(!chain.verify());
}
#[test]
fn test_witness_chain_finalize() {
let mut chain = WitnessChain::new();
chain.append("op1");
chain.append("op2");
let hex = chain.finalize();
assert_eq!(hex.len(), 64); // 32 bytes hex-encoded
assert_ne!(hex, "0".repeat(64));
}
#[test]
fn test_witness_chain_empty() {
let chain = WitnessChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
assert!(chain.verify()); // empty chain is valid
assert_eq!(chain.finalize(), "0".repeat(64));
}
#[test]
fn test_witness_hash_backward_compat() {
let hash = witness_hash(&["op1", "op2"]);
assert_eq!(hash.len(), 64);
assert_ne!(hash, "0".repeat(64));
}
#[test]
fn test_build_witness_chain() {
let chain = BrainPipeline::build_witness_chain(&["a", "b", "c"]);
assert_eq!(chain.len(), 3);
assert!(chain.verify());
}
#[test]
fn test_pipeline_default() {
let pipeline = BrainPipeline::default();
// Default should work identically to new()
let clean = pipeline.strip_pii("key sk-test123");
assert!(clean.contains("[REDACTED_KEY]"));
}
}

View file

@ -0,0 +1,158 @@
//! MCP protocol server implementation (stdio JSON-RPC)
use crate::tools::McpBrainTools;
use crate::types::*;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::{debug, error, info, warn};
/// MCP Brain Server
pub struct McpBrainServer {
tools: McpBrainTools,
}
impl McpBrainServer {
pub fn new() -> Self {
Self {
tools: McpBrainTools::new(),
}
}
pub fn with_backend_url(url: String) -> Self {
Self {
tools: McpBrainTools::with_backend_url(url),
}
}
/// Run the server on stdio
pub async fn run_stdio(&self) -> Result<(), std::io::Error> {
info!("Starting MCP Brain server on stdio");
let stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let reader = BufReader::new(stdin);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
if line.trim().is_empty() {
continue;
}
debug!("Received: {}", line);
let response = self.handle_message(&line).await;
if let Some(resp) = response {
let resp_json = serde_json::to_string(&resp).unwrap_or_default();
debug!("Sending: {}", resp_json);
stdout.write_all(resp_json.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
}
}
info!("MCP Brain server shutting down");
Ok(())
}
async fn handle_message(&self, message: &str) -> Option<JsonRpcResponse> {
let request: JsonRpcRequest = match serde_json::from_str(message) {
Ok(req) => req,
Err(e) => {
error!("Failed to parse request: {}", e);
return Some(JsonRpcResponse::error(
serde_json::Value::Null,
-32700,
format!("Parse error: {}", e),
));
}
};
Some(self.handle_request(&request).await)
}
async fn handle_request(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
match request.method.as_str() {
"initialize" => self.handle_initialize(request),
"initialized" => JsonRpcResponse::success(request.id.clone(), serde_json::json!({})),
"tools/list" => self.handle_tools_list(request),
"tools/call" => self.handle_tools_call(request).await,
"shutdown" => {
info!("Received shutdown request");
JsonRpcResponse::success(request.id.clone(), serde_json::json!({}))
}
_ => {
warn!("Unknown method: {}", request.method);
JsonRpcResponse::error(
request.id.clone(),
-32601,
format!("Method not found: {}", request.method),
)
}
}
}
fn handle_initialize(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
info!("Handling initialize request");
JsonRpcResponse::success(
request.id.clone(),
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": false }
},
"serverInfo": {
"name": "mcp-brain",
"version": env!("CARGO_PKG_VERSION")
}
}),
)
}
fn handle_tools_list(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
info!("Handling tools/list request");
let tools = McpBrainTools::list_tools();
JsonRpcResponse::success(request.id.clone(), serde_json::json!({ "tools": tools }))
}
async fn handle_tools_call(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
info!("Handling tools/call request");
let tool_call: McpToolCall = match serde_json::from_value(request.params.clone()) {
Ok(tc) => tc,
Err(e) => {
return JsonRpcResponse::error(
request.id.clone(),
-32602,
format!("Invalid params: {}", e),
);
}
};
match self.tools.call_tool(tool_call).await {
Ok(result) => {
let response_content = match result {
McpToolResult::Success { content } => serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&content).unwrap_or_default()
}]
}),
McpToolResult::Error { error } => serde_json::json!({
"content": [{
"type": "text",
"text": error
}],
"isError": true
}),
};
JsonRpcResponse::success(request.id.clone(), response_content)
}
Err(e) => JsonRpcResponse::error(request.id.clone(), e.code(), e.to_string()),
}
}
}
impl Default for McpBrainServer {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,784 @@
//! MCP tools for the shared brain
use crate::client::BrainClient;
use crate::embed::BrainEmbedder;
use crate::pipeline::BrainPipeline;
use crate::types::*;
use tracing::info;
/// Error type for brain operations
#[derive(Debug, thiserror::Error)]
pub enum BrainError {
#[error("Client error: {0}")]
Client(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Pipeline error: {0}")]
Pipeline(String),
}
impl BrainError {
pub fn code(&self) -> i32 {
match self {
BrainError::Client(_) => -32001,
BrainError::InvalidRequest(_) => -32602,
BrainError::NotFound(_) => -32002,
BrainError::Pipeline(_) => -32003,
}
}
}
/// Brain tools handler
pub struct McpBrainTools {
client: BrainClient,
pipeline: BrainPipeline,
embedder: std::sync::Mutex<BrainEmbedder>,
}
impl McpBrainTools {
pub fn new() -> Self {
Self {
client: BrainClient::new(),
pipeline: BrainPipeline::new(),
embedder: std::sync::Mutex::new(BrainEmbedder::new()),
}
}
pub fn with_backend_url(url: String) -> Self {
Self {
client: BrainClient::with_url(url),
pipeline: BrainPipeline::new(),
embedder: std::sync::Mutex::new(BrainEmbedder::new()),
}
}
/// Get list of all tools (core + Brainpedia + WASM)
pub fn list_tools() -> Vec<McpTool> {
vec![
McpTool {
name: "brain_share".to_string(),
description: "Share a learning with the collective brain. Knowledge is PII-stripped, embedded, signed, and stored as an RVF cognitive container.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["architecture", "pattern", "solution", "convention", "security", "performance", "tooling", "debug"], "description": "Knowledge category" },
"title": { "type": "string", "description": "Short title (max 200 chars)" },
"content": { "type": "string", "description": "Knowledge content (max 10000 chars)" },
"tags": { "type": "array", "items": { "type": "string" }, "description": "Tags (max 10, each max 30 chars)" },
"code_snippet": { "type": "string", "description": "Optional code snippet" }
},
"required": ["category", "title", "content"]
}),
},
McpTool {
name: "brain_search".to_string(),
description: "Semantic search across shared knowledge. Returns ranked results with quality scores and drift warnings.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" },
"category": { "type": "string", "description": "Filter by category" },
"tags": { "type": "string", "description": "Comma-separated tags to filter" },
"limit": { "type": "integer", "description": "Max results (default 10)" },
"min_quality": { "type": "number", "description": "Minimum quality score (0-1)" }
},
"required": ["query"]
}),
},
McpTool {
name: "brain_get".to_string(),
description: "Retrieve a specific memory with full provenance including witness chain and quality history.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Memory ID (UUID)" }
},
"required": ["id"]
}),
},
McpTool {
name: "brain_vote".to_string(),
description: "Vote on a memory's quality (Bayesian update). Affects ranking and contributor reputation.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Memory ID" },
"direction": { "type": "string", "enum": ["up", "down"], "description": "Vote direction" }
},
"required": ["id", "direction"]
}),
},
McpTool {
name: "brain_transfer".to_string(),
description: "Apply learned priors from one knowledge domain to another. Uses Meta Thompson Sampling with dampened transfer.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"source_domain": { "type": "string", "description": "Source knowledge domain" },
"target_domain": { "type": "string", "description": "Target knowledge domain" }
},
"required": ["source_domain", "target_domain"]
}),
},
McpTool {
name: "brain_drift".to_string(),
description: "Check if shared knowledge has drifted from expected distributions. Reports coefficient of variation and trend.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"domain": { "type": "string", "description": "Domain to check (optional, default: global)" },
"since": { "type": "string", "description": "ISO timestamp to check from" }
}
}),
},
McpTool {
name: "brain_partition".to_string(),
description: "Get knowledge partitioned by mincut topology. Shows emergent knowledge clusters with coherence scores.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"domain": { "type": "string", "description": "Domain to partition" },
"min_cluster_size": { "type": "integer", "description": "Minimum memories per cluster" }
}
}),
},
McpTool {
name: "brain_list".to_string(),
description: "List recent shared memories, optionally filtered by category and quality.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"category": { "type": "string", "description": "Filter by category" },
"limit": { "type": "integer", "description": "Max results (default 20)" },
"min_quality": { "type": "number", "description": "Minimum quality score" }
}
}),
},
McpTool {
name: "brain_delete".to_string(),
description: "Delete your own contribution. Only the original contributor can delete.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Memory ID to delete" }
},
"required": ["id"]
}),
},
McpTool {
name: "brain_status".to_string(),
description: "Get system health: memory count, contributor count, graph topology, drift status, and quality metrics.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
McpTool {
name: "brain_sync".to_string(),
description: "Sync local MicroLoRA weights with the shared brain. Downloads consensus weights, applies locally, exports local deltas, submits to server for federated aggregation.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"direction": { "type": "string", "enum": ["pull", "push", "both"], "description": "Sync direction (default: both)" }
}
}),
},
// ── Brainpedia (ADR-062) ───────────────────────────────────
McpTool {
name: "brain_page_create".to_string(),
description: "Create a new Brainpedia page (Draft). Requires reputation >= 0.5. Pages go through Draft → Canonical lifecycle with evidence gating.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["architecture", "pattern", "solution", "convention", "security", "performance", "tooling", "debug"], "description": "Knowledge category" },
"title": { "type": "string", "description": "Page title (max 200 chars)" },
"content": { "type": "string", "description": "Page content (max 10000 chars)" },
"tags": { "type": "array", "items": { "type": "string" }, "description": "Tags (max 10)" },
"code_snippet": { "type": "string", "description": "Optional code snippet" },
"evidence_links": { "type": "array", "description": "Initial evidence links" }
},
"required": ["category", "title", "content"]
}),
},
McpTool {
name: "brain_page_get".to_string(),
description: "Get a Brainpedia page with its full delta log, evidence links, and promotion status.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Page ID (UUID)" }
},
"required": ["id"]
}),
},
McpTool {
name: "brain_page_delta".to_string(),
description: "Submit a delta (correction, extension, or deprecation) to an existing Brainpedia page. Requires evidence links.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"page_id": { "type": "string", "description": "Page ID (UUID)" },
"delta_type": { "type": "string", "enum": ["correction", "extension", "evidence", "deprecation"], "description": "Type of delta" },
"content_diff": { "type": "object", "description": "Content changes" },
"evidence_links": { "type": "array", "description": "Supporting evidence" }
},
"required": ["page_id", "delta_type", "content_diff"]
}),
},
McpTool {
name: "brain_page_deltas".to_string(),
description: "List all deltas for a Brainpedia page, showing its modification history.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"page_id": { "type": "string", "description": "Page ID (UUID)" }
},
"required": ["page_id"]
}),
},
McpTool {
name: "brain_page_evidence".to_string(),
description: "Add evidence to a Brainpedia page. Evidence types: test_pass, build_success, metric_improval, peer_review.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"page_id": { "type": "string", "description": "Page ID (UUID)" },
"evidence": { "type": "object", "description": "Evidence link with type, description, and verification data" }
},
"required": ["page_id", "evidence"]
}),
},
McpTool {
name: "brain_page_promote".to_string(),
description: "Promote a Draft page to Canonical. Requires: quality >= 0.7, observations >= 5, evidence >= 3 from >= 2 contributors.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"page_id": { "type": "string", "description": "Page ID (UUID)" }
},
"required": ["page_id"]
}),
},
// ── WASM Executable Nodes (ADR-063) ────────────────────────
McpTool {
name: "brain_node_list".to_string(),
description: "List all published (non-revoked) WASM executable nodes in the brain.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
McpTool {
name: "brain_node_publish".to_string(),
description: "Publish a new WASM executable node. V1 ABI requires: memory, malloc, feature_extract_dim, feature_extract exports. Includes conformance test vectors.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Node ID (e.g., 'my-feature-extractor')" },
"name": { "type": "string", "description": "Human-readable name" },
"version": { "type": "string", "description": "Semver version" },
"dim": { "type": "integer", "description": "Output dimension (default 128)" },
"exports": { "type": "array", "items": { "type": "string" }, "description": "WASM exports" },
"interface": { "type": "object", "description": "Interface specification" },
"conformance": { "type": "array", "description": "Conformance test vectors" },
"wasm_bytes": { "type": "string", "description": "Base64-encoded WASM binary" },
"signature": { "type": "string", "description": "Ed25519 signature (hex)" }
},
"required": ["id", "name", "version", "exports", "wasm_bytes", "signature"]
}),
},
McpTool {
name: "brain_node_get".to_string(),
description: "Get WASM node metadata and conformance test vectors.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Node ID" }
},
"required": ["id"]
}),
},
McpTool {
name: "brain_node_wasm".to_string(),
description: "Download WASM binary for a node. Returns base64-encoded bytes.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Node ID" }
},
"required": ["id"]
}),
},
McpTool {
name: "brain_node_revoke".to_string(),
description: "Revoke a WASM node (original publisher only). Marks as revoked but retains bytes for forensic analysis.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Node ID to revoke" }
},
"required": ["id"]
}),
},
]
}
/// Handle a tool call
pub async fn call_tool(&self, call: McpToolCall) -> Result<McpToolResult, BrainError> {
info!("Calling tool: {}", call.name);
match call.name.as_str() {
"brain_share" => self.brain_share(call.arguments).await,
"brain_search" => self.brain_search(call.arguments).await,
"brain_get" => self.brain_get(call.arguments).await,
"brain_vote" => self.brain_vote(call.arguments).await,
"brain_transfer" => self.brain_transfer(call.arguments).await,
"brain_drift" => self.brain_drift(call.arguments).await,
"brain_partition" => self.brain_partition(call.arguments).await,
"brain_list" => self.brain_list(call.arguments).await,
"brain_delete" => self.brain_delete(call.arguments).await,
"brain_status" => self.brain_status(call.arguments).await,
"brain_sync" => self.brain_sync(call.arguments).await,
// Brainpedia (ADR-062)
"brain_page_create" => self.brain_page_create(call.arguments).await,
"brain_page_get" => self.brain_page_get(call.arguments).await,
"brain_page_delta" => self.brain_page_delta(call.arguments).await,
"brain_page_deltas" => self.brain_page_deltas(call.arguments).await,
"brain_page_evidence" => self.brain_page_evidence(call.arguments).await,
"brain_page_promote" => self.brain_page_promote(call.arguments).await,
// WASM Executable Nodes (ADR-063)
"brain_node_list" => self.brain_node_list(call.arguments).await,
"brain_node_publish" => self.brain_node_publish(call.arguments).await,
"brain_node_get" => self.brain_node_get(call.arguments).await,
"brain_node_wasm" => self.brain_node_wasm(call.arguments).await,
"brain_node_revoke" => self.brain_node_revoke(call.arguments).await,
_ => Err(BrainError::InvalidRequest(format!("Unknown tool: {}", call.name))),
}
}
async fn brain_share(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let category = args.get("category").and_then(|v| v.as_str()).unwrap_or("pattern");
let title = args.get("title").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("title required".into()))?;
let content = args.get("content").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("content required".into()))?;
let tags: Vec<String> = args.get("tags")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
let code_snippet = args.get("code_snippet").and_then(|v| v.as_str()).map(String::from);
// PII strip all user-provided text
let clean_title = self.pipeline.strip_pii(title);
let clean_content = self.pipeline.strip_pii(content);
let clean_tags: Vec<String> = tags.iter()
.map(|t| self.pipeline.strip_pii(t))
.collect();
let clean_snippet = code_snippet.as_deref().map(|s| self.pipeline.strip_pii(s));
// Safety check: reject if PII still detected after stripping
if self.pipeline.contains_pii(&clean_title) {
return Err(BrainError::Pipeline("PII detected in title after stripping".into()));
}
if self.pipeline.contains_pii(&clean_content) {
return Err(BrainError::Pipeline("PII detected in content after stripping".into()));
}
for tag in &clean_tags {
if self.pipeline.contains_pii(tag) {
return Err(BrainError::Pipeline("PII detected in tags after stripping".into()));
}
}
if let Some(ref s) = clean_snippet {
if self.pipeline.contains_pii(s) {
return Err(BrainError::Pipeline("PII detected in code_snippet after stripping".into()));
}
}
// Generate embedding via structured hash + MicroLoRA
let _embedding = if let Ok(mut emb) = self.embedder.lock() {
emb.embed(&clean_content)
} else {
crate::embed::generate_embedding(&clean_content)
};
// Build witness chain: pii_strip -> embed -> share
let mut chain = crate::pipeline::WitnessChain::new();
chain.append("pii_strip");
chain.append("embed");
chain.append("share");
let _witness_hash = chain.finalize();
let result = self.client.share(
category,
&clean_title,
&clean_content,
&clean_tags,
clean_snippet.as_deref(),
).await.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(result).unwrap_or_default(),
})
}
async fn brain_search(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let query = args.get("query").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("query required".into()))?;
let category = args.get("category").and_then(|v| v.as_str());
let tags = args.get("tags").and_then(|v| v.as_str());
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
let min_quality = args.get("min_quality").and_then(|v| v.as_f64());
// Generate query embedding via structured hash + MicroLoRA
let _query_embedding = if let Ok(mut emb) = self.embedder.lock() {
emb.embed(query)
} else {
crate::embed::generate_embedding(query)
};
let results = self.client.search(query, category, tags, limit, min_quality).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(results).unwrap_or_default(),
})
}
async fn brain_get(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
let memory = self.client.get(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(memory).unwrap_or_default(),
})
}
async fn brain_vote(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
let direction = args.get("direction").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("direction required".into()))?;
let result = self.client.vote(id, direction).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(result).unwrap_or_default(),
})
}
async fn brain_transfer(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let source = args.get("source_domain").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("source_domain required".into()))?;
let target = args.get("target_domain").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("target_domain required".into()))?;
let result = self.client.transfer(source, target).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(result).unwrap_or_default(),
})
}
async fn brain_drift(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let domain = args.get("domain").and_then(|v| v.as_str());
let since = args.get("since").and_then(|v| v.as_str());
let report = self.client.drift(domain, since).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(report).unwrap_or_default(),
})
}
async fn brain_partition(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let domain = args.get("domain").and_then(|v| v.as_str());
let min_size = args.get("min_cluster_size").and_then(|v| v.as_u64()).map(|v| v as usize);
let result = self.client.partition(domain, min_size).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(result).unwrap_or_default(),
})
}
async fn brain_list(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let category = args.get("category").and_then(|v| v.as_str());
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
let results = self.client.list(category, limit).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(results).unwrap_or_default(),
})
}
async fn brain_delete(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
self.client.delete(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::json!({"deleted": true, "id": id}),
})
}
async fn brain_status(&self, _args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let status = self.client.status().await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::to_value(status).unwrap_or_default(),
})
}
async fn brain_sync(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let direction = args.get("direction").and_then(|v| v.as_str()).unwrap_or("both");
let mut pulled = false;
let mut pushed = false;
// Pull: download consensus weights from server
if direction == "pull" || direction == "both" {
match self.client.lora_latest().await {
Ok(Some(weights)) => {
if let Ok(mut emb) = self.embedder.lock() {
emb.import_consensus_weights(weights);
pulled = true;
}
}
Ok(None) => {
// No consensus weights available yet — that's fine
}
Err(e) => {
info!("Failed to pull consensus weights: {e}");
}
}
}
// Push: export local weights and submit to server
if direction == "push" || direction == "both" {
let local_weights = if let Ok(emb) = self.embedder.lock() {
emb.export_local_weights()
} else {
None
};
if let Some(mut weights) = local_weights {
weights.clip();
if weights.validate().is_ok() {
match self.client.lora_submit(&weights).await {
Ok(_) => { pushed = true; }
Err(e) => {
info!("Failed to push local weights: {e}");
}
}
}
}
}
let embed_count = if let Ok(emb) = self.embedder.lock() {
emb.embed_count()
} else {
0
};
Ok(McpToolResult::Success {
content: serde_json::json!({
"pulled": pulled,
"pushed": pushed,
"direction": direction,
"local_embed_count": embed_count,
}),
})
}
// ── Brainpedia (ADR-062) ─────────────────────────────────────────
async fn brain_page_create(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let category = args.get("category").and_then(|v| v.as_str()).unwrap_or("pattern");
let title = args.get("title").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("title required".into()))?;
let content = args.get("content").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("content required".into()))?;
let tags: Vec<String> = args.get("tags")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
let code_snippet = args.get("code_snippet").and_then(|v| v.as_str());
let evidence_links = args.get("evidence_links").cloned().unwrap_or(serde_json::json!([]));
let clean_title = self.pipeline.strip_pii(title);
let clean_content = self.pipeline.strip_pii(content);
let clean_tags: Vec<String> = tags.iter().map(|t| self.pipeline.strip_pii(t)).collect();
let embedding = if let Ok(mut emb) = self.embedder.lock() {
emb.embed(&clean_content)
} else {
crate::embed::generate_embedding(&clean_content)
};
let mut chain = crate::pipeline::WitnessChain::new();
chain.append("pii_strip");
chain.append("embed");
chain.append("page_create");
let witness_hash = chain.finalize();
let body = serde_json::json!({
"category": category,
"title": clean_title,
"content": clean_content,
"tags": clean_tags,
"code_snippet": code_snippet,
"embedding": embedding,
"evidence_links": evidence_links,
"witness_hash": hex::encode(witness_hash),
});
let result = self.client.create_page(&body).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_page_get(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
let result = self.client.get_page(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_page_delta(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let page_id = args.get("page_id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("page_id required".into()))?;
let delta_type = args.get("delta_type").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("delta_type required".into()))?;
let content_diff = args.get("content_diff").cloned()
.ok_or_else(|| BrainError::InvalidRequest("content_diff required".into()))?;
let evidence_links = args.get("evidence_links").cloned().unwrap_or(serde_json::json!([]));
let mut chain = crate::pipeline::WitnessChain::new();
chain.append("delta_submit");
let witness_hash = chain.finalize();
let body = serde_json::json!({
"delta_type": delta_type,
"content_diff": content_diff,
"evidence_links": evidence_links,
"witness_hash": hex::encode(witness_hash),
});
let result = self.client.submit_delta(page_id, &body).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_page_deltas(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let page_id = args.get("page_id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("page_id required".into()))?;
let result = self.client.list_deltas(page_id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_page_evidence(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let page_id = args.get("page_id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("page_id required".into()))?;
let evidence = args.get("evidence").cloned()
.ok_or_else(|| BrainError::InvalidRequest("evidence required".into()))?;
let body = serde_json::json!({ "evidence": evidence });
let result = self.client.add_evidence(page_id, &body).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_page_promote(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let page_id = args.get("page_id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("page_id required".into()))?;
let result = self.client.promote_page(page_id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
// ── WASM Executable Nodes (ADR-063) ───────────────────────────────
async fn brain_node_list(&self, _args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let result = self.client.list_nodes().await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_node_publish(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let result = self.client.publish_node(&args).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_node_get(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
let result = self.client.get_node(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success { content: result })
}
async fn brain_node_wasm(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
let bytes = self.client.get_node_wasm(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
Ok(McpToolResult::Success {
content: serde_json::json!({
"id": id,
"wasm_bytes_b64": b64,
"size_bytes": bytes.len(),
}),
})
}
async fn brain_node_revoke(&self, args: serde_json::Value) -> Result<McpToolResult, BrainError> {
let id = args.get("id").and_then(|v| v.as_str())
.ok_or_else(|| BrainError::InvalidRequest("id required".into()))?;
self.client.revoke_node(id).await
.map_err(|e| BrainError::Client(e.to_string()))?;
Ok(McpToolResult::Success {
content: serde_json::json!({"revoked": true, "id": id}),
})
}
}
impl Default for McpBrainTools {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,181 @@
//! Types for MCP Brain server
use serde::{Deserialize, Serialize};
/// Brain memory categories
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BrainCategory {
Architecture,
Pattern,
Solution,
Convention,
Security,
Performance,
Tooling,
Debug,
Custom(String),
}
/// Brain memory (local representation)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainMemory {
pub id: String,
pub category: BrainCategory,
pub title: String,
pub content: String,
pub tags: Vec<String>,
pub code_snippet: Option<String>,
pub quality_score: f64,
pub contributor_id: String,
pub partition_id: Option<u32>,
pub created_at: String,
pub updated_at: String,
}
/// Drift report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriftReport {
pub domain: Option<String>,
pub coefficient_of_variation: f64,
pub is_drifting: bool,
pub delta_sparsity: f64,
pub trend: String,
pub suggested_action: String,
pub window_size: usize,
}
/// Partition result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionResult {
pub clusters: Vec<KnowledgeCluster>,
pub cut_value: f64,
pub total_memories: usize,
}
/// Knowledge cluster
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeCluster {
pub id: u32,
pub memory_ids: Vec<String>,
pub dominant_category: BrainCategory,
pub size: usize,
pub coherence: f64,
}
/// Status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusInfo {
pub total_memories: usize,
pub total_contributors: usize,
pub graph_nodes: usize,
pub graph_edges: usize,
pub cluster_count: usize,
pub avg_quality: f64,
pub drift_status: String,
}
/// Transfer result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferResult {
pub source_domain: String,
pub target_domain: String,
pub acceleration_factor: f64,
pub transfer_success: bool,
pub message: String,
}
/// Vote direction
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VoteDirection {
Up,
Down,
}
/// Quality score (Bayesian Beta distribution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BetaParams {
pub alpha: f64,
pub beta: f64,
}
// ============== JSON-RPC Protocol Types (cloned from mcp-gate) ==============
/// MCP Tool definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub name: String,
pub description: String,
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Value,
}
/// MCP Tool call request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCall {
pub name: String,
pub arguments: serde_json::Value,
}
/// MCP Tool result
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpToolResult {
Success { content: serde_json::Value },
Error { error: String },
}
/// JSON-RPC request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: serde_json::Value,
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}
/// JSON-RPC response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
/// JSON-RPC error
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcResponse {
pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn error(id: serde_json::Value, code: i32, message: String) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code,
message,
data: None,
}),
}
}
}

View file

@ -1,5 +1,6 @@
# Multi-stage Dockerfile for ruvector-postgres extension
# Builds the extension and creates a PostgreSQL image with it installed
# v0.3.1: Fixes — Cypher self-reference, graph/RDF persistence, SONA dimension panic
# Build stage
# Using nightly Rust to support edition2024 crates in the registry
@ -101,8 +102,8 @@ COPY crates/rvf/rvf-types /workspace/crates/rvf/rvf-types/
COPY crates/rvf/rvf-wire /workspace/crates/rvf/rvf-wire/
COPY crates/rvf/rvf-crypto /workspace/crates/rvf/rvf-crypto/
# Use the workspace Cargo.lock to pin dependencies and avoid registry parsing issues
COPY Cargo.lock /workspace/crates/ruvector-postgres/
# Copy the workspace Cargo.lock to pin dependency versions
COPY Cargo.lock /workspace/Cargo.lock
WORKDIR /workspace/crates/ruvector-postgres
@ -117,7 +118,8 @@ RUN cargo fetch
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git
# Build the extension with all features including v0.3 modules
RUN cargo pgrx package --features "pg17 index-all quant-all embeddings gated-transformer analytics-complete attention-extended sona-learning domain-expansion"
# graph-complete includes: graph, hyperbolic, sparse
RUN cargo pgrx package --features "pg17 index-all quant-all graph-complete embeddings gated-transformer analytics-complete attention-extended sona-learning domain-expansion"
# Build the model downloader binary
RUN cargo build --release --bin download-models --features "embeddings"
@ -130,25 +132,31 @@ RUN mkdir -p /opt/ruvector/models && \
echo "Model cache size: $(du -sh /opt/ruvector/models)" && \
ls -la /opt/ruvector/models/
# Copy the pre-built SQL schema file (with sparse functions removed)
# cargo pgrx schema doesn't work reliably in Docker, so we use the hand-crafted file
RUN cp /workspace/crates/ruvector-postgres/sql/ruvector--0.1.0.sql /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--0.1.0.sql && \
echo "SQL schema copied with $(grep -c 'CREATE FUNCTION\|CREATE OR REPLACE FUNCTION' /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--0.1.0.sql) functions"
# Copy all SQL schema files (control file default_version=0.3.0 selects the right one)
RUN for f in /workspace/crates/ruvector-postgres/sql/ruvector--*.sql; do \
cp "$f" /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ ; \
done && \
echo "SQL schemas copied:" && \
ls -1 /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--*.sql && \
echo "v0.3.0 function count: $(grep -c 'CREATE FUNCTION\|CREATE OR REPLACE FUNCTION' /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--0.3.0.sql)"
# Verify the extension files are complete
RUN ls -la /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ && \
echo "=== First 20 lines of SQL ===" && \
head -20 /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--0.1.0.sql && \
echo "=== CREATE FUNCTION count ===" && \
grep -c "CREATE FUNCTION\|CREATE OR REPLACE FUNCTION" /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector--0.1.0.sql
echo "=== Extension control ===" && \
cat /workspace/target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector.control
# Runtime stage
FROM postgres:17-bookworm
# Labels
LABEL maintainer="ruvector team"
LABEL description="PostgreSQL with ruvector extension - high-performance vector similarity search with local embeddings"
LABEL version="0.3.0"
LABEL maintainer="ruvector team <info@ruv.io>"
LABEL description="PostgreSQL with ruvector extension - high-performance vector database with 270+ SQL functions, Graph/Cypher/SPARQL, GNN, hybrid search, multi-tenancy, self-healing, SONA self-learning, and local embeddings"
LABEL version="0.3.1"
LABEL org.opencontainers.image.title="ruvector-postgres"
LABEL org.opencontainers.image.version="0.3.1"
LABEL org.opencontainers.image.vendor="ruv.io"
LABEL org.opencontainers.image.source="https://github.com/ruvnet/ruvector"
LABEL org.opencontainers.image.description="Drop-in pgvector replacement with SIMD, Flash Attention, GNN, Cypher, SPARQL, hybrid search, multi-tenancy, self-healing, and SONA"
# Set embedding model cache path - models are pre-downloaded during build
# FASTEMBED_CACHE_DIR is the correct env var for fastembed-rs

View file

@ -0,0 +1,54 @@
# Slim Dockerfile for ruvector-postgres — uses pre-compiled extension artifacts
# Build time: ~30 seconds (no Rust compilation)
#
# Pre-requisite: Run locally first:
# cargo pgrx package -p ruvector-postgres --pg-config /usr/lib/postgresql/17/bin/pg_config \
# --features "pg17,index-all,quant-all,graph-complete,gated-transformer,analytics-complete,attention-extended,sona-learning,domain-expansion"
#
# Then build:
# docker build -t ruvnet/ruvector-postgres:0.3.1 -f crates/ruvector-postgres/Dockerfile.prebuilt .
FROM postgres:17-bookworm
# Labels
LABEL maintainer="ruvector team <info@ruv.io>"
LABEL description="PostgreSQL with ruvector extension — 270+ SQL functions, Graph/Cypher/SPARQL, GNN, hybrid search, multi-tenancy, self-healing, SONA self-learning"
LABEL version="0.3.1"
LABEL org.opencontainers.image.title="ruvector-postgres"
LABEL org.opencontainers.image.version="0.3.1"
LABEL org.opencontainers.image.vendor="ruv.io"
LABEL org.opencontainers.image.source="https://github.com/ruvnet/ruvector"
LABEL org.opencontainers.image.description="Drop-in pgvector replacement with SIMD, Flash Attention, GNN, Cypher, SPARQL, hybrid search, multi-tenancy, self-healing, and SONA"
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Copy pre-built extension shared library
COPY target/release/ruvector-pg17/usr/lib/postgresql/17/lib/* \
/usr/lib/postgresql/17/lib/
# Copy extension control and SQL files
COPY target/release/ruvector-pg17/usr/share/postgresql/17/extension/ruvector.control \
/usr/share/postgresql/17/extension/
COPY crates/ruvector-postgres/sql/ruvector--0.1.0.sql \
crates/ruvector-postgres/sql/ruvector--0.3.0.sql \
crates/ruvector-postgres/sql/ruvector--2.0.0.sql \
crates/ruvector-postgres/sql/ruvector--2.0.0--0.3.0.sql \
/usr/share/postgresql/17/extension/
# Copy initialization script
COPY crates/ruvector-postgres/docker/init.sql /docker-entrypoint-initdb.d/01-init.sql
# Environment
ENV POSTGRES_USER=ruvector
ENV POSTGRES_PASSWORD=ruvector
ENV POSTGRES_DB=ruvector
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD pg_isready -U $POSTGRES_USER -d $POSTGRES_DB || exit 1
EXPOSE 5432
CMD ["postgres"]

View file

@ -1,7 +1,7 @@
# RuVector-Postgres Development & Testing Dockerfile
# Multi-stage build with PostgreSQL version support (14-17)
# Default: PostgreSQL 17 (latest with pgrx 0.12 support)
# Note: PostgreSQL 18 requires pgrx 0.15.0+ (planned for future release)
# v0.3.1: Fixes — Cypher self-reference, graph/RDF persistence, SONA dimension panic
ARG PG_VERSION=17
ARG RUST_VERSION=1.85
@ -145,15 +145,18 @@ COPY crates/ruvector-postgres/sql ./sql/
COPY crates/ruvector-postgres/benches ./benches/
# Build the extension with all features including v0.3 modules
# graph-complete includes: graph, hyperbolic, sparse
RUN cargo pgrx package \
--pg-config /usr/lib/postgresql/${PG_VERSION}/bin/pg_config \
--features pg${PG_VERSION},graph-complete,gated-transformer,analytics-complete,attention-extended,sona-learning,domain-expansion
--features pg${PG_VERSION},index-all,quant-all,graph-complete,gated-transformer,analytics-complete,attention-extended,sona-learning,domain-expansion
# pgrx generates .control and .so but not SQL - copy our hand-written SQL files
# In a workspace, target/ is at the workspace root /build/target/, not per-crate
RUN cp sql/ruvector--0.3.0.sql /build/target/release/ruvector-pg${PG_VERSION}/usr/share/postgresql/${PG_VERSION}/extension/ 2>/dev/null || true && \
cp sql/ruvector--2.0.0.sql /build/target/release/ruvector-pg${PG_VERSION}/usr/share/postgresql/${PG_VERSION}/extension/ 2>/dev/null || true && \
cp sql/ruvector--2.0.0--0.3.0.sql /build/target/release/ruvector-pg${PG_VERSION}/usr/share/postgresql/${PG_VERSION}/extension/ 2>/dev/null || true
RUN for f in sql/ruvector--*.sql; do \
cp "$f" /build/target/release/ruvector-pg${PG_VERSION}/usr/share/postgresql/${PG_VERSION}/extension/ 2>/dev/null || true; \
done && \
echo "SQL schemas copied:" && \
ls -1 /build/target/release/ruvector-pg${PG_VERSION}/usr/share/postgresql/${PG_VERSION}/extension/ruvector--*.sql 2>/dev/null
# ============================================================================
# Stage 4: Runtime (Production)
@ -184,9 +187,9 @@ ENV PG_VERSION=${PG_VERSION}
ENV POSTGRES_INITDB_ARGS="--data-checksums"
# Labels for version tracking
LABEL org.opencontainers.image.title="RuVector PostgreSQL Extension v0.3"
LABEL org.opencontainers.image.description="High-performance vector database extension for PostgreSQL with 143 SQL functions, Solver, Math, TDA, Extended Attention, Sona, and Domain Expansion"
LABEL org.opencontainers.image.version="0.3.0"
LABEL org.opencontainers.image.title="RuVector PostgreSQL Extension v0.3.1"
LABEL org.opencontainers.image.description="High-performance vector database extension for PostgreSQL with 270+ SQL functions, Graph/Cypher/SPARQL, GNN, hybrid search, multi-tenancy, self-healing, SONA self-learning, Solver, Math, TDA, and Domain Expansion"
LABEL org.opencontainers.image.version="0.3.1"
LABEL org.opencontainers.image.vendor="ruv.io"
LABEL org.opencontainers.image.source="https://github.com/ruvnet/ruvector"
LABEL ruvector.pg.version="${PG_VERSION}"

View file

@ -14,7 +14,7 @@ version: '3.8'
# Build arguments shared across services
x-build-args: &build-args
PG_VERSION: ${PG_VERSION:-17}
RUST_VERSION: ${RUST_VERSION:-1.83}
RUST_VERSION: ${RUST_VERSION:-1.85}
# Common environment for test containers
x-test-env: &test-env
@ -179,7 +179,7 @@ services:
dockerfile: crates/ruvector-postgres/docker/Dockerfile
args:
PG_VERSION: 14
RUST_VERSION: ${RUST_VERSION:-1.83}
RUST_VERSION: ${RUST_VERSION:-1.85}
container_name: ruvector-postgres-pg14
ports:
- "5414:5432"
@ -205,7 +205,7 @@ services:
dockerfile: crates/ruvector-postgres/docker/Dockerfile
args:
PG_VERSION: 15
RUST_VERSION: ${RUST_VERSION:-1.83}
RUST_VERSION: ${RUST_VERSION:-1.85}
container_name: ruvector-postgres-pg15
ports:
- "5415:5432"
@ -231,7 +231,7 @@ services:
dockerfile: crates/ruvector-postgres/docker/Dockerfile
args:
PG_VERSION: 16
RUST_VERSION: ${RUST_VERSION:-1.83}
RUST_VERSION: ${RUST_VERSION:-1.85}
container_name: ruvector-postgres-pg16
ports:
- "5416:5432"

View file

@ -1,7 +1,8 @@
-- RuVector PostgreSQL Extension v0.3
-- RuVector PostgreSQL Extension v0.3.1
-- Version: 0.3.0
-- High-performance vector similarity search with SIMD optimizations
-- Features: 270+ SQL functions, Solver, Math, TDA, Extended Attention, Sona, Domain Expansion
-- Features: 190 SQL functions — Solver, Math, TDA, Attention, GNN, Self-Healing,
-- Multi-Tenancy, Hybrid Search, Graph/Cypher/SPARQL, Sona, Domain Expansion
-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION ruvector" to load this file. \quit
@ -479,9 +480,36 @@ LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- ============================================================================
-- GNN (Graph Neural Network) Functions
-- ============================================================================
-- Note: GCN and GraphSAGE functions are auto-generated by pgrx with JsonB signature
-- The functions ruvector_gcn_forward and ruvector_graphsage_forward use JsonB types
-- and are defined in src/gnn/operators.rs with #[pg_extern] macro
-- GCN forward pass on node embeddings
CREATE OR REPLACE FUNCTION ruvector_gcn_forward(embeddings_json jsonb, src integer[], dst integer[], weights real[] DEFAULT NULL, out_dim integer DEFAULT 0)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_gcn_forward_wrapper'
LANGUAGE C IMMUTABLE PARALLEL SAFE;
-- Aggregate neighbor messages (sum, mean, max)
CREATE OR REPLACE FUNCTION ruvector_gnn_aggregate(messages_json jsonb, method text)
RETURNS real[]
AS 'MODULE_PATHNAME', 'ruvector_gnn_aggregate_wrapper'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- Multi-hop message passing over graph
CREATE OR REPLACE FUNCTION ruvector_message_pass(node_table text, edge_table text, embedding_col text, hops integer, layer_type text)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_message_pass_wrapper'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- GraphSAGE forward pass with neighbor sampling
CREATE OR REPLACE FUNCTION ruvector_graphsage_forward(embeddings_json jsonb, src integer[], dst integer[], out_dim integer, num_samples integer)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_graphsage_forward_wrapper'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- Batch GNN inference on multiple graphs
CREATE OR REPLACE FUNCTION ruvector_gnn_batch_forward(embeddings_batch_json jsonb, edge_indices_batch integer[], graph_sizes integer[], layer_type text, out_dim integer)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_gnn_batch_forward_wrapper'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- ============================================================================
-- Routing/Agent Functions (Tiny Dancer)
@ -727,6 +755,264 @@ RETURNS boolean
AS 'MODULE_PATHNAME', 'ruvector_sparql_update_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- ============================================================================
-- Self-Healing Functions (23 functions)
-- ============================================================================
-- Get current health status
CREATE OR REPLACE FUNCTION ruvector_health_status()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_health_status_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Check if system is healthy
CREATE OR REPLACE FUNCTION ruvector_is_healthy()
RETURNS boolean
AS 'MODULE_PATHNAME', 'ruvector_is_healthy_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get system metrics for problem detection
CREATE OR REPLACE FUNCTION ruvector_system_metrics()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_system_metrics_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get recent healing history
CREATE OR REPLACE FUNCTION ruvector_healing_history(lim integer DEFAULT 20)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_history_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get healing history since timestamp
CREATE OR REPLACE FUNCTION ruvector_healing_history_since(since_timestamp bigint)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_history_since_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Get healing history for a strategy
CREATE OR REPLACE FUNCTION ruvector_healing_history_for_strategy(strategy_name text, lim integer DEFAULT 20)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_history_for_strategy_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Manually trigger healing for a problem type
CREATE OR REPLACE FUNCTION ruvector_healing_trigger(problem_type text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_trigger_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Execute a specific healing strategy
CREATE OR REPLACE FUNCTION ruvector_healing_execute(strategy_name text, problem_type text, dry_run boolean DEFAULT false)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_execute_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Configure healing engine settings
CREATE OR REPLACE FUNCTION ruvector_healing_configure(config_json jsonb)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_configure_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Get current healing configuration
CREATE OR REPLACE FUNCTION ruvector_healing_get_config()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_get_config_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Enable or disable healing
CREATE OR REPLACE FUNCTION ruvector_healing_enable(enabled boolean)
RETURNS boolean
AS 'MODULE_PATHNAME', 'ruvector_healing_enable_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- List all healing strategies
CREATE OR REPLACE FUNCTION ruvector_healing_strategies()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_strategies_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get effectiveness report
CREATE OR REPLACE FUNCTION ruvector_healing_effectiveness()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_effectiveness_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get healing engine statistics
CREATE OR REPLACE FUNCTION ruvector_healing_stats()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_stats_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get detection thresholds
CREATE OR REPLACE FUNCTION ruvector_healing_thresholds()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_thresholds_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Update detection thresholds
CREATE OR REPLACE FUNCTION ruvector_healing_set_thresholds(thresholds_json jsonb)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_set_thresholds_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- List all supported problem types
CREATE OR REPLACE FUNCTION ruvector_healing_problem_types()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_healing_problem_types_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- ============================================================================
-- Multi-Tenancy Functions (17 functions)
-- ============================================================================
-- Create a new tenant
CREATE OR REPLACE FUNCTION ruvector_tenant_create(tenant_id text, config jsonb DEFAULT NULL)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_create_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Set current tenant context
CREATE OR REPLACE FUNCTION ruvector_tenant_set(tenant_id text)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_set_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Get tenant statistics
CREATE OR REPLACE FUNCTION ruvector_tenant_stats(tenant_id text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenant_stats_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Check tenant quota status
CREATE OR REPLACE FUNCTION ruvector_tenant_quota_check(tenant_id text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenant_quota_check_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Suspend a tenant
CREATE OR REPLACE FUNCTION ruvector_tenant_suspend(tenant_id text)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_suspend_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Resume a suspended tenant
CREATE OR REPLACE FUNCTION ruvector_tenant_resume(tenant_id text)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_resume_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Delete a tenant
CREATE OR REPLACE FUNCTION ruvector_tenant_delete(tenant_id text, hard boolean DEFAULT false)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_delete_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- List all tenants
CREATE OR REPLACE FUNCTION ruvector_tenants()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenants_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Enable tenant RLS on a table
CREATE OR REPLACE FUNCTION ruvector_enable_tenant_rls(table_name text, tenant_column text DEFAULT 'tenant_id')
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_enable_tenant_rls_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Migrate tenant to new isolation level
CREATE OR REPLACE FUNCTION ruvector_tenant_migrate(tenant_id text, target_level text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenant_migrate_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Get tenant migration status
CREATE OR REPLACE FUNCTION ruvector_tenant_migration_status(tenant_id text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenant_migration_status_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Isolate tenant to dedicated resources
CREATE OR REPLACE FUNCTION ruvector_tenant_isolate(tenant_id text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_tenant_isolate_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Set promotion policy for auto isolation upgrades
CREATE OR REPLACE FUNCTION ruvector_tenant_set_policy(policy_config jsonb)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_set_policy_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Update tenant quota
CREATE OR REPLACE FUNCTION ruvector_tenant_update_quota(tenant_id text, quota_config jsonb)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_tenant_update_quota_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Generate RLS setup SQL for a table
CREATE OR REPLACE FUNCTION ruvector_generate_rls_sql(table_name text, tenant_column text DEFAULT 'tenant_id')
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_generate_rls_sql_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Generate SQL to add tenant column
CREATE OR REPLACE FUNCTION ruvector_generate_tenant_column_sql(table_name text, column_name text DEFAULT 'tenant_id', not_null boolean DEFAULT true, auto_default boolean DEFAULT true)
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_generate_tenant_column_sql_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Generate SQL to create ruvector roles
CREATE OR REPLACE FUNCTION ruvector_generate_roles_sql()
RETURNS text
AS 'MODULE_PATHNAME', 'ruvector_generate_roles_sql_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- ============================================================================
-- Hybrid Search Functions (7 functions)
-- ============================================================================
-- Register collection for hybrid search
CREATE OR REPLACE FUNCTION ruvector_register_hybrid(collection text, vector_column text, fts_column text, text_column text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_register_hybrid_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Update BM25 corpus statistics
CREATE OR REPLACE FUNCTION ruvector_hybrid_update_stats(collection text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_hybrid_update_stats_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Configure hybrid search settings
CREATE OR REPLACE FUNCTION ruvector_hybrid_configure(collection text, config jsonb)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_hybrid_configure_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Perform hybrid search (BM25 + vector)
CREATE OR REPLACE FUNCTION ruvector_hybrid_search(collection text, query_text text, query_vector real[], k integer, fusion text DEFAULT NULL, alpha real DEFAULT NULL)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_hybrid_search_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- Get hybrid search statistics
CREATE OR REPLACE FUNCTION ruvector_hybrid_stats(collection text)
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_hybrid_stats_wrapper'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
-- Compute hybrid score from vector distance and keyword score
CREATE OR REPLACE FUNCTION ruvector_hybrid_score(vector_distance real, keyword_score real, alpha real DEFAULT 0.5)
RETURNS real
AS 'MODULE_PATHNAME', 'ruvector_hybrid_score_wrapper'
LANGUAGE C IMMUTABLE PARALLEL SAFE;
-- List all hybrid-enabled collections
CREATE OR REPLACE FUNCTION ruvector_hybrid_list()
RETURNS jsonb
AS 'MODULE_PATHNAME', 'ruvector_hybrid_list_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;
-- ============================================================================
-- Comments
-- ============================================================================

View file

@ -5,6 +5,8 @@ use crate::graph::storage::GraphStore;
use serde_json::{json, Value as JsonValue};
use std::collections::HashMap;
// Direction is re-exported from ast::*
/// Execute a parsed Cypher query
pub fn execute_cypher(
graph: &GraphStore,
@ -94,41 +96,183 @@ fn match_pattern(
pattern: &Pattern,
context: &mut ExecutionContext,
) -> Result<(), String> {
// Simple implementation: match nodes by label and properties
// Collect the pattern as alternating nodes and relationships:
// (a:Person)-[:KNOWS]->(b:Person) = [Node(a), Rel(KNOWS), Node(b)]
let mut node_patterns: Vec<&NodePattern> = Vec::new();
let mut rel_patterns: Vec<&RelationshipPattern> = Vec::new();
for element in &pattern.elements {
match element {
PatternElement::Node(node_pattern) => {
match_node(graph, node_pattern, context)?;
PatternElement::Node(np) => node_patterns.push(np),
PatternElement::Relationship(rp) => rel_patterns.push(rp),
}
}
// Case 1: Single node pattern — find all matching nodes
if rel_patterns.is_empty() {
if let Some(np) = node_patterns.first() {
let candidates = find_matching_nodes(graph, np);
if candidates.is_empty() {
return Ok(());
}
PatternElement::Relationship(rel_pattern) => {
match_relationship(graph, rel_pattern, context)?;
// Create a binding row per matching node
let mut rows: Vec<HashMap<String, Binding>> = Vec::new();
for node in &candidates {
let mut row = HashMap::new();
if let Some(var) = &np.variable {
row.insert(var.clone(), Binding::Node(node.id));
}
rows.push(row);
}
context.bindings = rows;
return Ok(());
}
return Ok(());
}
// Case 2: Pattern with relationships — traverse edges
// For each relationship pattern, we need pairs of (source_node, target_node)
// Pattern: (a)-[r:TYPE]->(b) means: find all edges of TYPE,
// check source matches a's pattern and target matches b's pattern
let mut result_rows: Vec<HashMap<String, Binding>> = Vec::new();
// We process node-rel-node triples
for i in 0..rel_patterns.len() {
let src_pattern = node_patterns.get(i);
let dst_pattern = node_patterns.get(i + 1);
let rel_pattern = rel_patterns[i];
// Get candidate edges by type
let edges = if let Some(ref rel_type) = rel_pattern.rel_type {
graph.edges.find_by_type(rel_type)
} else {
graph.edges.all_edges()
};
for edge in &edges {
let (src_id, dst_id) = match rel_pattern.direction {
Direction::Outgoing | Direction::Both => (edge.source, edge.target),
Direction::Incoming => (edge.target, edge.source),
};
// Check source node matches pattern
if let Some(sp) = src_pattern {
if !node_matches_pattern(graph, src_id, sp) {
continue;
}
}
// Check target node matches pattern
if let Some(dp) = dst_pattern {
if !node_matches_pattern(graph, dst_id, dp) {
continue;
}
}
// Reject self-references when variables are different
if let (Some(sp), Some(dp)) = (src_pattern, dst_pattern) {
if let (Some(sv), Some(dv)) = (&sp.variable, &dp.variable) {
if sv != dv && src_id == dst_id {
continue;
}
}
}
// Build binding row
let mut row = HashMap::new();
if let Some(sp) = src_pattern {
if let Some(var) = &sp.variable {
row.insert(var.clone(), Binding::Node(src_id));
}
}
if let Some(dp) = dst_pattern {
if let Some(var) = &dp.variable {
row.insert(var.clone(), Binding::Node(dst_id));
}
}
if let Some(var) = &rel_pattern.variable {
row.insert(var.clone(), Binding::Edge(edge.id));
}
result_rows.push(row);
// Also match the reverse direction for Both
if rel_pattern.direction == Direction::Both && edge.source != edge.target {
let mut rev_row = HashMap::new();
if let Some(sp) = src_pattern {
if let Some(var) = &sp.variable {
rev_row.insert(var.clone(), Binding::Node(edge.target));
}
}
if let Some(dp) = dst_pattern {
if let Some(var) = &dp.variable {
rev_row.insert(var.clone(), Binding::Node(edge.source));
}
}
if let Some(var) = &rel_pattern.variable {
rev_row.insert(var.clone(), Binding::Edge(edge.id));
}
if let (Some(sp), Some(dp)) = (src_pattern, dst_pattern) {
if node_matches_pattern(graph, edge.target, sp)
&& node_matches_pattern(graph, edge.source, dp)
{
result_rows.push(rev_row);
}
}
}
}
}
if !result_rows.is_empty() {
context.bindings = result_rows;
}
Ok(())
}
fn match_node(
/// Find all nodes matching a node pattern (labels + properties)
fn find_matching_nodes(
graph: &GraphStore,
pattern: &NodePattern,
context: &mut ExecutionContext,
) -> Result<(), String> {
// Find nodes matching labels and properties
) -> Vec<crate::graph::storage::Node> {
let candidates = if pattern.labels.is_empty() {
graph.nodes.all_nodes()
} else {
// Find by first label
graph.nodes.find_by_label(&pattern.labels[0])
};
for node in candidates {
// Check additional labels
if !pattern.labels.iter().all(|l| node.has_label(l)) {
continue;
}
candidates
.into_iter()
.filter(|node| {
// Check all labels
if !pattern.labels.iter().all(|l| node.has_label(l)) {
return false;
}
// Check properties
pattern.properties.iter().all(|(key, expr)| {
if let Some(node_value) = node.get_property(key) {
if let Expression::Literal(expected) = expr {
node_value == expected
} else {
false
}
} else {
false
}
})
})
.collect()
}
/// Check if a specific node ID matches a node pattern
fn node_matches_pattern(graph: &GraphStore, node_id: u64, pattern: &NodePattern) -> bool {
if let Some(node) = graph.nodes.get(node_id) {
// Check labels
if !pattern.labels.iter().all(|l| node.has_label(l)) {
return false;
}
// Check properties
let matches_props = pattern.properties.iter().all(|(key, expr)| {
pattern.properties.iter().all(|(key, expr)| {
if let Some(node_value) = node.get_property(key) {
if let Expression::Literal(expected) = expr {
node_value == expected
@ -138,27 +282,10 @@ fn match_node(
} else {
false
}
});
if matches_props {
if let Some(var) = &pattern.variable {
context.bind(var, Binding::Node(node.id));
}
return Ok(());
}
})
} else {
false
}
Ok(())
}
fn match_relationship(
_graph: &GraphStore,
_pattern: &RelationshipPattern,
_context: &mut ExecutionContext,
) -> Result<(), String> {
// Simplified relationship matching
// Production code would traverse the graph based on relationship pattern
Ok(())
}
fn execute_create(
@ -228,9 +355,6 @@ fn create_relationship(
source_id: u64,
context: &ExecutionContext,
) -> Result<u64, String> {
// Simplified: assumes target node is bound in context
// Production code would handle more complex patterns
let mut properties = HashMap::new();
for (key, expr) in &pattern.properties {
@ -243,8 +367,18 @@ fn create_relationship(
.clone()
.unwrap_or_else(|| "RELATED".to_string());
// For now, create a self-loop. Production code would get target from pattern
let target_id = source_id;
// Resolve target from the next node in the pattern (bound in context)
// Look through bindings for any node binding that isn't the source
let target_id = context
.bindings
.iter()
.rev()
.flat_map(|b| b.values())
.find_map(|binding| match binding {
Binding::Node(id) if *id != source_id => Some(*id),
_ => None,
})
.unwrap_or(source_id);
graph.add_edge(source_id, target_id, edge_type, properties)
}

View file

@ -1,6 +1,7 @@
// Graph operations module for ruvector-postgres
//
// Provides graph storage, traversal, Cypher query support, and SPARQL (W3C standard)
// Graph and RDF data is persisted to PostgreSQL tables for durability across connections.
pub mod cypher;
pub mod operators;
@ -13,33 +14,271 @@ pub use storage::{Edge, EdgeStore, GraphStore, Node, NodeStore};
pub use traversal::{bfs, dfs, shortest_path_dijkstra, PathResult};
use dashmap::DashMap;
use pgrx::JsonB;
use std::collections::HashMap;
use std::sync::Arc;
/// Global graph storage registry
/// Global graph storage registry (in-memory cache, backed by PG tables)
static GRAPH_REGISTRY: once_cell::sync::Lazy<DashMap<String, Arc<GraphStore>>> =
once_cell::sync::Lazy::new(|| DashMap::new());
/// Get or create a graph by name
/// Ensure persistence tables exist (idempotent)
fn ensure_graph_tables() {
use pgrx::prelude::*;
Spi::run(
"CREATE TABLE IF NOT EXISTS _ruvector_graphs (
name TEXT PRIMARY KEY
)",
)
.ok();
Spi::run(
"CREATE TABLE IF NOT EXISTS _ruvector_nodes (
graph_name TEXT NOT NULL REFERENCES _ruvector_graphs(name) ON DELETE CASCADE,
id BIGINT NOT NULL,
labels TEXT[] NOT NULL DEFAULT '{}',
properties JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (graph_name, id)
)",
)
.ok();
Spi::run(
"CREATE TABLE IF NOT EXISTS _ruvector_edges (
graph_name TEXT NOT NULL REFERENCES _ruvector_graphs(name) ON DELETE CASCADE,
id BIGINT NOT NULL,
source BIGINT NOT NULL,
target BIGINT NOT NULL,
edge_type TEXT NOT NULL,
properties JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (graph_name, id)
)",
)
.ok();
}
/// Load a graph from PostgreSQL tables into the in-memory cache
fn load_graph_from_tables(name: &str) -> Option<Arc<GraphStore>> {
use pgrx::prelude::*;
use serde_json::Value as JsonValue;
// Check if graph exists in tables
let exists = Spi::get_one_with_args::<bool>(
"SELECT EXISTS(SELECT 1 FROM _ruvector_graphs WHERE name = $1)",
vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())],
)
.ok()
.flatten()
.unwrap_or(false);
if !exists {
return None;
}
let graph = Arc::new(GraphStore::new());
// Load nodes
let _ = Spi::connect(|client| {
let tup_table = client.select(
"SELECT id, labels, properties FROM _ruvector_nodes WHERE graph_name = $1 ORDER BY id",
None,
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)?;
for row in tup_table {
let id: i64 = row.get_by_name::<i64, _>("id")?.unwrap_or(0);
let labels: Vec<String> = row
.get_by_name::<Vec<String>, _>("labels")?
.unwrap_or_default();
let props_json: JsonB = row
.get_by_name::<JsonB, _>("properties")?
.unwrap_or(JsonB(serde_json::json!({})));
let props: HashMap<String, JsonValue> =
if let JsonValue::Object(map) = props_json.0 {
map.into_iter().collect()
} else {
HashMap::new()
};
let mut node = Node::new(id as u64);
node.labels = labels;
node.properties = props;
graph.nodes.insert(node);
// Advance the ID counter past loaded IDs
while graph.nodes.next_id() <= id as u64 {
// next_id auto-increments
}
}
Ok::<_, spi::Error>(())
});
// Load edges
let _ = Spi::connect(|client| {
let tup_table = client.select(
"SELECT id, source, target, edge_type, properties FROM _ruvector_edges WHERE graph_name = $1 ORDER BY id",
None,
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)?;
for row in tup_table {
let id: i64 = row.get_by_name::<i64, _>("id")?.unwrap_or(0);
let source: i64 = row.get_by_name::<i64, _>("source")?.unwrap_or(0);
let target: i64 = row.get_by_name::<i64, _>("target")?.unwrap_or(0);
let edge_type: String = row
.get_by_name::<String, _>("edge_type")?
.unwrap_or_default();
let props_json: JsonB = row
.get_by_name::<JsonB, _>("properties")?
.unwrap_or(JsonB(serde_json::json!({})));
let props: HashMap<String, JsonValue> =
if let JsonValue::Object(map) = props_json.0 {
map.into_iter().collect()
} else {
HashMap::new()
};
let mut edge = Edge::new(id as u64, source as u64, target as u64, edge_type);
edge.properties = props;
graph.edges.insert(edge);
while graph.edges.next_id() <= id as u64 {}
}
Ok::<_, spi::Error>(())
});
GRAPH_REGISTRY.insert(name.to_string(), graph.clone());
Some(graph)
}
/// Persist a graph entry to the backing table
fn persist_graph_name(name: &str) {
use pgrx::prelude::*;
Spi::run_with_args(
"INSERT INTO _ruvector_graphs (name) VALUES ($1) ON CONFLICT DO NOTHING",
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)
.ok();
}
/// Persist a node to the backing table
pub fn persist_node(graph_name: &str, node: &Node) {
use pgrx::prelude::*;
let props = JsonB(serde_json::to_value(&node.properties).unwrap_or_default());
Spi::run_with_args(
"INSERT INTO _ruvector_nodes (graph_name, id, labels, properties)
VALUES ($1, $2, $3, $4)
ON CONFLICT (graph_name, id) DO UPDATE SET labels = $3, properties = $4",
Some(vec![
(PgBuiltInOids::TEXTOID.oid(), graph_name.into_datum()),
(PgBuiltInOids::INT8OID.oid(), (node.id as i64).into_datum()),
(
PgBuiltInOids::TEXTARRAYOID.oid(),
node.labels.clone().into_datum(),
),
(PgBuiltInOids::JSONBOID.oid(), props.into_datum()),
]),
)
.ok();
}
/// Persist an edge to the backing table
pub fn persist_edge(graph_name: &str, edge: &Edge) {
use pgrx::prelude::*;
let props = JsonB(serde_json::to_value(&edge.properties).unwrap_or_default());
Spi::run_with_args(
"INSERT INTO _ruvector_edges (graph_name, id, source, target, edge_type, properties)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (graph_name, id) DO UPDATE SET source = $3, target = $4, edge_type = $5, properties = $6",
Some(vec![
(PgBuiltInOids::TEXTOID.oid(), graph_name.into_datum()),
(PgBuiltInOids::INT8OID.oid(), (edge.id as i64).into_datum()),
(
PgBuiltInOids::INT8OID.oid(),
(edge.source as i64).into_datum(),
),
(
PgBuiltInOids::INT8OID.oid(),
(edge.target as i64).into_datum(),
),
(
PgBuiltInOids::TEXTOID.oid(),
edge.edge_type.clone().into_datum(),
),
(PgBuiltInOids::JSONBOID.oid(), props.into_datum()),
]),
)
.ok();
}
/// Get or create a graph by name (with persistence)
pub fn get_or_create_graph(name: &str) -> Arc<GraphStore> {
if let Some(g) = GRAPH_REGISTRY.get(name) {
return g.clone();
}
// Try loading from tables
ensure_graph_tables();
if let Some(g) = load_graph_from_tables(name) {
return g;
}
// Create new
persist_graph_name(name);
GRAPH_REGISTRY
.entry(name.to_string())
.or_insert_with(|| Arc::new(GraphStore::new()))
.clone()
}
/// Get an existing graph by name
/// Get an existing graph by name (checks tables if not in cache)
pub fn get_graph(name: &str) -> Option<Arc<GraphStore>> {
GRAPH_REGISTRY.get(name).map(|g| g.clone())
if let Some(g) = GRAPH_REGISTRY.get(name) {
return Some(g.clone());
}
// Try loading from persistent storage
ensure_graph_tables();
load_graph_from_tables(name)
}
/// Delete a graph by name
/// Delete a graph by name (from cache and tables)
pub fn delete_graph(name: &str) -> bool {
GRAPH_REGISTRY.remove(name).is_some()
use pgrx::prelude::*;
GRAPH_REGISTRY.remove(name);
// CASCADE deletes nodes and edges
Spi::run_with_args(
"DELETE FROM _ruvector_graphs WHERE name = $1",
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)
.ok();
true
}
/// List all graph names
/// List all graph names (from persistent storage)
pub fn list_graphs() -> Vec<String> {
GRAPH_REGISTRY.iter().map(|e| e.key().clone()).collect()
use pgrx::prelude::*;
ensure_graph_tables();
let mut names: Vec<String> = Vec::new();
let _ = Spi::connect(|client| {
let tup_table = client.select("SELECT name FROM _ruvector_graphs ORDER BY name", None, None)?;
for row in tup_table {
if let Some(name) = row.get_by_name::<String, _>("name")? {
names.push(name);
}
}
Ok::<_, spi::Error>(())
});
// Also include any in-memory-only graphs
for entry in GRAPH_REGISTRY.iter() {
if !names.contains(entry.key()) {
names.push(entry.key().clone());
}
}
names
}
#[cfg(test)]
@ -47,17 +286,16 @@ mod tests {
use super::*;
#[test]
fn test_graph_registry() {
let graph1 = get_or_create_graph("test_graph");
let graph2 = get_graph("test_graph");
fn test_graph_registry_in_memory() {
// Pure in-memory test (no PG context)
let graph = Arc::new(GraphStore::new());
GRAPH_REGISTRY.insert("unit_test_graph".to_string(), graph.clone());
assert!(graph2.is_some());
assert!(Arc::ptr_eq(&graph1, &graph2.unwrap()));
let g2 = GRAPH_REGISTRY.get("unit_test_graph").map(|g| g.clone());
assert!(g2.is_some());
assert!(Arc::ptr_eq(&graph, &g2.unwrap()));
let graphs = list_graphs();
assert!(graphs.contains(&"test_graph".to_string()));
assert!(delete_graph("test_graph"));
assert!(get_graph("test_graph").is_none());
GRAPH_REGISTRY.remove("unit_test_graph");
assert!(GRAPH_REGISTRY.get("unit_test_graph").is_none());
}
}

View file

@ -156,6 +156,11 @@ fn ruvector_add_node(
let node_id = graph.add_node(labels, props);
// Persist to backing table
if let Some(node) = graph.nodes.get(node_id) {
super::persist_node(graph_name, &node);
}
Ok(node_id as i64)
}
@ -189,6 +194,11 @@ fn ruvector_add_edge(
props,
)?;
// Persist to backing table
if let Some(edge) = graph.edges.get(edge_id) {
super::persist_edge(graph_name, &edge);
}
Ok(edge_id as i64)
}
@ -407,7 +417,10 @@ fn ruvector_insert_triple(
let store = get_or_create_store(store_name);
let triple = Triple::from_strings(subject, predicate, object);
let id = store.insert(triple);
let id = store.insert(triple.clone());
// Persist to backing table
super::sparql::persist_triple(store_name, id, &triple, None);
Ok(id as i64)
}
@ -435,7 +448,10 @@ fn ruvector_insert_triple_graph(
let store = get_or_create_store(store_name);
let triple = Triple::from_strings(subject, predicate, object);
let id = store.insert_into_graph(triple, Some(graph));
let id = store.insert_into_graph(triple.clone(), Some(graph));
// Persist to backing table
super::sparql::persist_triple(store_name, id, &triple, Some(graph));
Ok(id as i64)
}
@ -479,7 +495,8 @@ fn ruvector_load_ntriples(store_name: &str, ntriples: &str) -> Result<i64, Strin
};
let triple = Triple::from_strings(subject, predicate, object);
store.insert(triple);
let id = store.insert(triple.clone());
super::sparql::persist_triple(store_name, id, &triple, None);
count += 1;
}
}

View file

@ -39,34 +39,172 @@ use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::sync::Arc;
/// Global RDF triple store registry
/// Global RDF triple store registry (in-memory cache, backed by PG tables)
static TRIPLE_STORE_REGISTRY: Lazy<DashMap<String, Arc<TripleStore>>> =
Lazy::new(|| DashMap::new());
/// Get or create a triple store by name
/// Ensure RDF persistence tables exist (idempotent)
fn ensure_rdf_tables() {
use pgrx::prelude::*;
Spi::run(
"CREATE TABLE IF NOT EXISTS _ruvector_rdf_stores (
name TEXT PRIMARY KEY
)",
)
.ok();
Spi::run(
"CREATE TABLE IF NOT EXISTS _ruvector_triples (
store_name TEXT NOT NULL REFERENCES _ruvector_rdf_stores(name) ON DELETE CASCADE,
id BIGINT NOT NULL,
subject TEXT NOT NULL,
predicate TEXT NOT NULL,
object TEXT NOT NULL,
graph_name TEXT,
PRIMARY KEY (store_name, id)
)",
)
.ok();
}
/// Load a triple store from PostgreSQL tables
fn load_store_from_tables(name: &str) -> Option<Arc<TripleStore>> {
use pgrx::prelude::*;
let exists = Spi::get_one_with_args::<bool>(
"SELECT EXISTS(SELECT 1 FROM _ruvector_rdf_stores WHERE name = $1)",
vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())],
)
.ok()
.flatten()
.unwrap_or(false);
if !exists {
return None;
}
let store = Arc::new(TripleStore::new());
let _ = pgrx::prelude::Spi::connect(|client| {
let tup_table = client.select(
"SELECT id, subject, predicate, object, graph_name FROM _ruvector_triples WHERE store_name = $1 ORDER BY id",
None,
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)?;
for row in tup_table {
let subject: String = row.get_by_name::<String, _>("subject")?.unwrap_or_default();
let predicate: String = row.get_by_name::<String, _>("predicate")?.unwrap_or_default();
let object: String = row.get_by_name::<String, _>("object")?.unwrap_or_default();
let graph_name: Option<String> = row.get_by_name::<String, _>("graph_name")?;
let triple = Triple::from_strings(&subject, &predicate, &object);
store.insert_into_graph(triple, graph_name.as_deref());
}
Ok::<_, pgrx::spi::Error>(())
});
TRIPLE_STORE_REGISTRY.insert(name.to_string(), store.clone());
Some(store)
}
/// Persist a triple to the backing table
pub fn persist_triple(store_name: &str, id: u64, triple: &Triple, graph: Option<&str>) {
use pgrx::prelude::*;
let subj = triple_store::term_to_key(&triple.subject);
let pred = triple.predicate.as_str().to_string();
let obj = triple_store::term_to_key(&triple.object);
Spi::run_with_args(
"INSERT INTO _ruvector_triples (store_name, id, subject, predicate, object, graph_name)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (store_name, id) DO NOTHING",
Some(vec![
(PgBuiltInOids::TEXTOID.oid(), store_name.into_datum()),
(PgBuiltInOids::INT8OID.oid(), (id as i64).into_datum()),
(PgBuiltInOids::TEXTOID.oid(), subj.into_datum()),
(PgBuiltInOids::TEXTOID.oid(), pred.into_datum()),
(PgBuiltInOids::TEXTOID.oid(), obj.into_datum()),
(
PgBuiltInOids::TEXTOID.oid(),
graph.map(|g| g.to_string()).into_datum(),
),
]),
)
.ok();
}
/// Get or create a triple store by name (with persistence)
pub fn get_or_create_store(name: &str) -> Arc<TripleStore> {
if let Some(s) = TRIPLE_STORE_REGISTRY.get(name) {
return s.clone();
}
ensure_rdf_tables();
if let Some(s) = load_store_from_tables(name) {
return s;
}
// Create new
use pgrx::prelude::*;
Spi::run_with_args(
"INSERT INTO _ruvector_rdf_stores (name) VALUES ($1) ON CONFLICT DO NOTHING",
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)
.ok();
TRIPLE_STORE_REGISTRY
.entry(name.to_string())
.or_insert_with(|| Arc::new(TripleStore::new()))
.clone()
}
/// Get an existing triple store by name
/// Get an existing triple store by name (checks tables if not in cache)
pub fn get_store(name: &str) -> Option<Arc<TripleStore>> {
TRIPLE_STORE_REGISTRY.get(name).map(|s| s.clone())
if let Some(s) = TRIPLE_STORE_REGISTRY.get(name) {
return Some(s.clone());
}
ensure_rdf_tables();
load_store_from_tables(name)
}
/// Delete a triple store by name
/// Delete a triple store by name (from cache and tables)
pub fn delete_store(name: &str) -> bool {
TRIPLE_STORE_REGISTRY.remove(name).is_some()
use pgrx::prelude::*;
TRIPLE_STORE_REGISTRY.remove(name);
// CASCADE deletes triples
Spi::run_with_args(
"DELETE FROM _ruvector_rdf_stores WHERE name = $1",
Some(vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())]),
)
.ok();
true
}
/// List all triple store names
/// List all triple store names (from persistent storage)
pub fn list_stores() -> Vec<String> {
TRIPLE_STORE_REGISTRY
.iter()
.map(|e| e.key().clone())
.collect()
use pgrx::prelude::*;
ensure_rdf_tables();
let mut names: Vec<String> = Vec::new();
let _ = Spi::connect(|client| {
let tup_table =
client.select("SELECT name FROM _ruvector_rdf_stores ORDER BY name", None, None)?;
for row in tup_table {
if let Some(name) = row.get_by_name::<String, _>("name")? {
names.push(name);
}
}
Ok::<_, pgrx::spi::Error>(())
});
for entry in TRIPLE_STORE_REGISTRY.iter() {
if !names.contains(entry.key()) {
names.push(entry.key().clone());
}
}
names
}
/// SPARQL error type

View file

@ -575,7 +575,7 @@ impl Default for TripleStore {
}
/// Convert an RDF term to a string key for indexing
fn term_to_key(term: &RdfTerm) -> String {
pub fn term_to_key(term: &RdfTerm) -> String {
match term {
RdfTerm::Iri(iri) => format!("<{}>", iri.as_str()),
RdfTerm::Literal(lit) => {

View file

@ -6,18 +6,32 @@ use dashmap::DashMap;
use ruvector_sona::{SonaConfig, SonaEngine};
use std::sync::Arc;
/// Global Sona engine state per table.
/// Cache key includes dimension so different-dim inputs get separate engines.
fn engine_key(table_name: &str, dim: u32) -> String {
format!("{}::{}", table_name, dim)
}
/// Global Sona engine state per table+dimension.
static SONA_ENGINES: once_cell::sync::Lazy<DashMap<String, Arc<SonaEngine>>> =
once_cell::sync::Lazy::new(DashMap::new);
/// Get or create a SonaEngine for a given table.
/// Default dimension when none is specified (e.g., for stats queries).
const DEFAULT_DIM: u32 = 256;
/// Get or create a SonaEngine for a given table with default dimension.
pub fn get_or_create_engine(table_name: &str) -> Arc<SonaEngine> {
get_or_create_engine_with_dim(table_name, DEFAULT_DIM)
}
/// Get or create a SonaEngine for a given table and embedding dimension.
pub fn get_or_create_engine_with_dim(table_name: &str, dim: u32) -> Arc<SonaEngine> {
let key = engine_key(table_name, dim);
SONA_ENGINES
.entry(table_name.to_string())
.entry(key)
.or_insert_with(|| {
Arc::new(SonaEngine::with_config(SonaConfig {
hidden_dim: 256,
embedding_dim: 256,
hidden_dim: dim as usize,
embedding_dim: dim as usize,
..Default::default()
}))
})

View file

@ -8,7 +8,15 @@ use super::get_or_create_engine;
/// Record a learning trajectory for a table (Micro-LoRA).
#[pg_extern]
pub fn ruvector_sona_learn(table_name: &str, trajectory_json: JsonB) -> JsonB {
let engine = get_or_create_engine(table_name);
// Detect dimension from the trajectory data
let dim = trajectory_json
.0
.get("initial")
.and_then(|v| v.as_array())
.map(|arr| arr.len() as u32)
.unwrap_or(super::DEFAULT_DIM);
let engine = super::get_or_create_engine_with_dim(table_name, dim);
// Parse trajectory: {"initial": [f32...], "steps": [{"embedding": [f32...], "actions": [...], "reward": f32}]}
let initial: Vec<f32> = trajectory_json
@ -20,7 +28,7 @@ pub fn ruvector_sona_learn(table_name: &str, trajectory_json: JsonB) -> JsonB {
.filter_map(|x| x.as_f64().map(|f| f as f32))
.collect()
})
.unwrap_or_else(|| vec![0.0; 256]);
.unwrap_or_else(|| vec![0.0; dim as usize]);
let steps = trajectory_json
.0
@ -41,7 +49,7 @@ pub fn ruvector_sona_learn(table_name: &str, trajectory_json: JsonB) -> JsonB {
.filter_map(|x| x.as_f64().map(|f| f as f32))
.collect()
})
.unwrap_or_else(|| vec![0.0; 256]);
.unwrap_or_else(|| vec![0.0; dim as usize]);
let attention_weights: Vec<f32> = step
.get("attention_weights")
@ -75,19 +83,41 @@ pub fn ruvector_sona_learn(table_name: &str, trajectory_json: JsonB) -> JsonB {
}
/// Apply learned LoRA transformation to an embedding.
/// Dynamically matches engine dimension to input size.
#[pg_extern(immutable, parallel_safe)]
pub fn ruvector_sona_apply(table_name: &str, embedding: Vec<f32>) -> Vec<f32> {
let engine = get_or_create_engine(table_name);
let mut output = vec![0.0f32; embedding.len()];
engine.apply_micro_lora(&embedding, &mut output);
// If output is all zeros (no learned weights yet), return the input
if output.iter().all(|&x| x == 0.0) {
if embedding.is_empty() {
return embedding;
}
output
let dim = embedding.len() as u32;
let engine = super::get_or_create_engine_with_dim(table_name, dim);
let mut output = vec![0.0f32; embedding.len()];
// Guard against panics from the native engine
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
engine.apply_micro_lora(&embedding, &mut output);
}));
match result {
Ok(()) => {
// If output is all zeros (no learned weights yet), return the input
if output.iter().all(|&x| x == 0.0) {
embedding
} else {
output
}
}
Err(_) => {
// On panic, return input unchanged rather than crashing PostgreSQL
pgrx::warning!(
"SONA apply: internal error for dim={}, returning input unchanged",
dim
);
embedding
}
}
}
/// Get EWC++ forgetting metrics for a table.

View file

@ -84,8 +84,8 @@ pub use quantizer::{
absmean_ternary, quantize_tensor, LayerMask, Precision, PtBitnetConfig, TernaryFormat,
};
pub use rlm_embedder::{
BaseEmbedder, EmbeddingVariant, NeighborRetriever, RlmEmbedder, RlmEmbedderConfig,
RlmEmbeddingResult,
BaseEmbedder, EmbeddingVariant, FlatNeighborStore, HashEmbedder, NeighborRetriever,
RlmEmbedder, RlmEmbedderConfig, RlmEmbeddingResult,
};
pub use rlm_refiner::{RefinementResult, RefinementStepMetrics, RlmRefiner, RlmRefinerConfig};
pub use ternary_tensor::{pack_ternary, unpack_ternary, TernaryTensor};

View file

@ -650,6 +650,7 @@ impl<E: BaseEmbedder, R: NeighborRetriever> RlmEmbedder<E, R> {
/// this is for testing, benchmarking, and as a baseline.
///
/// On Pi 5: ~0.1ms per embedding (just hashing + normalize).
#[derive(Clone)]
pub struct HashEmbedder {
dim: usize,
}
@ -702,6 +703,7 @@ impl BaseEmbedder for HashEmbedder {
/// Suitable for small corpora (< 100K chunks) on Pi 5.
///
/// For larger corpora, use RuVector's HNSW index as the retriever.
#[derive(Clone)]
pub struct FlatNeighborStore {
chunks: Vec<StoredChunk>,
dim: usize,

View file

@ -0,0 +1,144 @@
# ADR-058: RVF Hash Security Hardening and Optimization
**Status**: Accepted
**Date**: 2026-02-27
**Authors**: ruv.io, RuVector Architecture Team
**Deciders**: Architecture Review Board
**SDK**: Claude-Flow
**Relates to**: ADR-029 (RVF Canonical Format), ADR-042 (Security-RVF-AIDefence-TEE)
## Context
### Current Hash Implementation
The RVF wire format (`rvf-wire`) uses XXH3-128 as the sole content hash for all
segment integrity verification. The `checksum_algo` field in the 64-byte segment
header supports three values:
| Algo | Name | Status |
|------|------|--------|
| 0 | CRC32C | Deprecated — silently upgraded to XXH3-128 |
| 1 | XXH3-128 | Active — used for all operations |
| 2 | SHAKE-256 | Declared in enum but **never implemented** |
### Security Findings
A comprehensive review of `rvf-wire/src/hash.rs`, `rvf-types/src/checksum.rs`,
and the graph shard module (`ruvector-graph/src/distributed/shard.rs`) identified
six issues:
1. **Non-constant-time hash comparison (P1)**: `verify_content_hash` uses `==`
on byte arrays. While XXH3-128 is not cryptographic, a timing side-channel
could reveal partial hash values to an attacker probing segment files over a
network interface. For defense-in-depth, verification should use
constant-time comparison.
2. **SHAKE-256 declared but unimplemented (P2)**: `ChecksumAlgo::Shake256` (algo=2)
exists in the enum and is accepted by `TryFrom<u8>`, but
`compute_content_hash` ignores the algo parameter entirely — all paths route
to XXH3-128. A writer could set `checksum_algo=2` in the header and it would
silently verify against XXH3-128, creating a false sense of cryptographic
integrity.
3. **Algo parameter ignored (P2)**: `compute_content_hash(_algo, data)` discards
the algorithm selector. If a future writer uses algo=2, the verifier cannot
detect the mismatch.
4. **No keyed/HMAC hash option (P3)**: The current scheme provides integrity
(accidental corruption detection) but not authentication. For federated
transfer scenarios (ADR-057), a keyed hash is needed to prevent a
man-in-the-middle from replacing segment payloads while recomputing the hash.
5. **Dead CRC32C dependency (P3)**: `crc32c = "0.6"` remains in `Cargo.toml`
even though CRC32C is fully deprecated. The `compute_crc32c` and
`compute_crc32c_hash` functions are dead code.
6. **Graph shard uses XXH3-64 (P3)**: `ruvector-graph` shard routing uses
`xxh3_64()` (64-bit). With 2^32 nodes the birthday bound gives ~50%
collision probability for shard assignment. This is acceptable for current
scale but noted for future growth.
### Performance Baseline
XXH3-128 on 1 MB payload: ~50 GB/s on AVX2 hardware (dominated by memory
bandwidth). No performance regression is expected from the changes below since
the hash function itself is not modified.
## Decision
### 1. Constant-Time Hash Verification
Replace the `==` comparison in `verify_content_hash` with a constant-time
byte-equality check using `subtle::ConstantTimeEq`. This eliminates the timing
side-channel at negligible cost (~2 ns overhead for 16 bytes).
### 2. Wire SHAKE-256 Implementation
Implement `compute_shake256_128(data)` using the `sha3` crate (already a
dependency of `rvf-crypto`). Route `algo=2` in `compute_content_hash` to this
implementation. This makes the `ChecksumAlgo::Shake256` enum variant truthful.
SHAKE-256 truncated to 128 bits provides:
- 128-bit collision resistance (same as XXH3-128)
- Post-quantum preimage resistance (vs XXH3's ~0 bits)
- ~300 MB/s throughput (vs XXH3's ~50 GB/s) — acceptable for
security-sensitive segments where correctness matters more than speed
### 3. Honor the Algo Parameter
Make `compute_content_hash` dispatch on the algo value:
- 0 → XXH3-128 (CRC32C upgrade, backward compatible)
- 1 → XXH3-128
- 2 → SHAKE-256 (first 128 bits)
- other → XXH3-128 (fallback)
### 4. Remove Dead CRC32C Code
Remove `compute_crc32c()`, `compute_crc32c_hash()`, and the `crc32c` dependency.
Retain the `Crc32c = 0` enum variant for backward-compatible header parsing.
### 5. Add Keyed Hash Support (Algo=3)
Reserve `checksum_algo=3` for HMAC-SHAKE-256 (keyed integrity). Implementation
is deferred to a follow-up PR as it requires key management infrastructure.
Add the enum variant now so the wire format is forward-compatible.
## Consequences
### Positive
- Eliminates timing side-channel in hash verification
- SHAKE-256 segments can now be written and verified correctly
- Dead code removed, smaller dependency tree
- Wire format is forward-compatible with keyed hashing
### Negative
- `subtle` crate added as a dependency (~10 KB, widely audited)
- `sha3` crate added to `rvf-wire` (already in `rvf-crypto`)
- Writers that relied on the silent algo-mismatch behavior will now produce
SHAKE-256 hashes when they set algo=2 (breaking change for any such writers,
but none are known to exist)
### Risks
- The `subtle` crate's constant-time guarantees depend on the compiler not
optimizing away the timing-safe operations. Rust's `subtle` v2.6+ uses
inline-asm barriers on supported platforms.
## Implementation Plan
1. Add `subtle` and `sha3` dependencies to `rvf-wire/Cargo.toml`
2. Remove `crc32c` dependency and dead CRC32C functions
3. Implement `compute_shake256_128()` in `hash.rs`
4. Update `compute_content_hash()` to dispatch on algo
5. Update `verify_content_hash()` to use `subtle::ConstantTimeEq`
6. Add `HmacShake256 = 3` to `ChecksumAlgo` enum (reserved, no impl yet)
7. Update tests and benchmarks
8. Verify existing tests pass (no behavioral change for algo=0 and algo=1)
## Integration with mcp-brain-server
ADR-075 documents the integration of `rvf-crypto` (including the SHAKE-256 functions hardened by this ADR) into the Shared Brain server. The brain server's `verify.rs` module previously used inline `sha3::Shake256` calls; it now delegates to `rvf_crypto::shake256_256()` for content hashing and `rvf_crypto::create_witness_chain()` / `rvf_crypto::verify_witness_chain()` for tamper-evident audit trails. This ensures that the constant-time comparison and proper algo dispatch implemented here are used consistently across the stack.
See: [ADR-075 — Wire Full RVF AGI Stack into mcp-brain-server](ADR-075-rvf-agi-stack-brain-integration.md)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,307 @@
# ADR-060: Shared Brain Capabilities — Federated MicroLoRA Intelligence Substrate
**Status**: Accepted
**Date**: 2026-02-27
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud Deployment), ADR-057 (Federated RVF Transfer Learning)
## 1. Context
The Shared Brain (ADR-059) turns isolated Claude Code sessions into a continuously improving, public, zero-trust learning substrate. The federated MicroLoRA communication bridge — where tiny shared weights (2KB per sync) move faster than any single model upgrade — enables seven distinct capabilities that were previously impossible with isolated sessions.
This ADR documents the sub-capabilities, practical use cases, business outcomes, and architectural considerations that emerge from the deployed system.
## 2. Core Capabilities
### 2.1 Cold Start Disappears
A brand-new session can land on the right approach in minutes because it downloads the current MicroLoRA consensus plus high-quality priors and policies. The "first time tax" — where every new repo, bug class, or architecture decision requires re-discovery — is eliminated.
**Mechanism**: `brain_sync(direction: "pull")` downloads consensus LoRA weights + `brain_search` retrieves relevant prior knowledge. The structured hash features provide immediate lexical n-gram-level matching; the MicroLoRA transform adds learned semantic refinement on top.
**Primary metric**: Median time to first relevant hit at top-5 on a fixed labeled query set, measured continuously. Secondary: median tool calls until first correct fix. Both tracked per-epoch to observe convergence.
**Measurable outcome**: New sessions converge on known solution patterns without rediscovering them. The acceleration is proportional to the quality and density of the knowledge graph in the relevant domain.
### 2.2 Debugging Becomes Cumulative
When one session finds a reliable fix pattern for a failure mode, the brain learns that trajectory. Future sessions retrieve it by meaning, not by exact text match. Debugging becomes a shared skill library that grows with every session.
**Mechanism**: The SONA engine records debugging trajectories as `LearnedPattern` entries with quality scores. Successful fixes (high quality feedback via `brain_vote(direction: "up")`) strengthen the corresponding embedding clusters. The MicroLoRA adapts to prefer embeddings near high-quality fix patterns.
**Example flow**:
1. Session A discovers that `tokio::spawn` deadlocks when combined with `parking_lot::RwLock` in certain patterns
2. Session A shares the fix pattern via `brain_share(category: "debug", ...)`
3. Session B encounters a similar deadlock
4. Session B's `brain_search` retrieves Session A's fix — not because the code is identical, but because the structured hash features capture the shared lexical tokens ("deadlock", "rwlock", "spawn") and the MicroLoRA has learned the semantic proximity between those tokens and the solution space
### 2.3 A Shared "Taste" for Good Solutions
Votes plus quality gating train the MicroLoRA to prefer solution patterns that actually worked downstream. Over time the system learns what "good" looks like for the ecosystem — not what is popular, but what is effective.
**Invariant: Quality signals are sourced only from authenticated contributors.** Voting is a write operation requiring API key authentication and contributor pseudonym derivation. This prevents the vote stream — which directly influences MicroLoRA gradient signals — from becoming the easiest poisoning path. Anonymous or unauthenticated users cannot influence quality scores.
**Mechanism**: The Bayesian quality score (`BetaParams { alpha, beta }`) accumulates vote evidence. Memories with `quality_score.mean() < 0.3` after `observations() >= 5` are auto-archived. The MicroLoRA's gradient signal is quality-weighted: high-quality patterns get stronger representation in the embedding space, shifting the consensus toward effective solutions.
**Anti-popularity bias**: Quality gating is based on Bayesian posteriors with flat priors, not raw vote counts. A memory with 3 upvotes and 0 downvotes (quality=0.8) is not assumed superior to one with 50 upvotes and 10 downvotes (quality=0.85). The Beta distribution's uncertainty naturally favors well-tested knowledge.
### 2.4 Transfer Learning Across Domains
TransferPrior and PolicyKernel segments enable moving learning from "code review" into "security audit" or from "infra ops" into "edge deployment" with measurable acceleration and confidence.
**Mechanism**: `brain_transfer(source_domain, target_domain)` uses `DomainExpansionEngine::initiate_transfer()` with sqrt-dampened priors (prevents overfit to source domain). `TransferVerification::verify()` requires both conditions: target improved AND source not regressed. `CostCurve` tracks the acceleration factor.
**Cross-domain examples**:
- Rust ownership patterns (source) -> TypeScript lifetime management patterns (target)
- REST API security (source) -> GraphQL security (target)
- Kubernetes deployment (source) -> Edge container deployment (target)
**Failure mode**: Transfer can inject incorrect analogies when domains are superficially similar but structurally different. Transfer priors are never auto-applied. They require explicit `brain_transfer` invocation, downstream verification in the target domain (target_after > target_before), and non-regression in the source domain (source_after >= source_before). If either condition fails, the transfer is rejected and the `TransferVerification.promotable` flag is false.
### 2.5 A Public Marketplace of Small, Auditable Intelligence Artifacts
Because everything is an RVF container with witness chains, knowledge can be exchanged as signed, replayable, revocable units. This is the basis for monetization, governance, and compliance.
**Canonical RVF container layout: exactly ten segments per memory** (from ADR-059 Section 6):
| # | Type | Content |
|---|------|---------|
| 1 | `MANIFEST (0x05)` | Segment directory |
| 2 | `VEC_SEG (0x01)` | SONA embedding (f32 x dim) |
| 3 | `META_SEG (0x07)` | Title, content, tags, category |
| 4 | `TransferPrior (0x30)` | Domain priors (if any) |
| 5 | `PolicyKernel (0x31)` | Best config snapshot |
| 6 | `CostCurve (0x32)` | Acceleration data |
| 7 | `FederatedManifest (0x33)` | Export metadata |
| 8 | `DiffPrivacyProof (0x34)` | Noise attestation |
| 9 | `RedactionLog (0x35)` | PII strip attestation |
| 10 | `WITNESS_SEG (0x0A)` | SHAKE-256 operation chain |
Ed25519 signature covers segments 1-10. This layout is locked — code comments and test assertions reference exactly ten segments.
**Auditability**: Every operation (share, vote, delete, transfer, sync) is recorded in the witness chain. Any participant can verify the chain independently.
### 2.6 An Anti-Poisoning Collective Memory
Robust aggregation, mincut partitioning, drift monitors, and reputation gating let the system absorb noisy public input without letting any single actor steer the centroid.
**Seven layers of defense**:
1. **Input sanitization**: 12-pattern PII strip, schema validation, size limits
2. **Server-side verification**: Re-check PII, witness chain, signature, embedding bounds
3. **DoS protection**: BudgetTokenBucket rate limiting, challenge nonces
4. **Byzantine aggregation**: 2-sigma outlier exclusion, per-parameter median + MAD trimming
5. **Multi-factor reputation**: `composite = accuracy^2 x uptime x stake_weight`, cold start at 0.1
6. **Drift monitoring**: VectorDelta centroid tracking, automatic rollback on excessive drift
7. **Vote authentication**: Quality signals sourced only from authenticated contributors (see Section 2.3)
**MicroLoRA-specific defenses**:
- Gate A policy validation: shape, NaN/Inf, [-2.0, 2.0] clipping bounds, L2 norm < 100, evidence >= 5
- Gate B robust aggregation: per-parameter median, 3xMAD trimming, reputation-weighted mean
- Consensus drift monitoring: L2 distance between raw (unclipped, unnormalized) consensus weight vectors across epochs; rollback threshold 5.0 calibrated for rank-2 on 128-dim (512 total parameters, weights clipped to [-2, 2] before submission). This threshold must be recalibrated if rank or dimension changes.
### 2.7 Always-On Agent Coordination
The brain becomes the local edge brain for real systems. It can watch logs, configs, incidents, and sensor streams, then pull collective learnings on demand while keeping sensitive raw data local.
**Architecture**: The MCP stdio server (`mcp-brain`) runs as a local process within each Claude Code session. It communicates with the Cloud Run backend (`mcp-brain-server`) via HTTPS. Raw data never leaves the local session — only distilled embeddings, LoRA weights, and sanitized metadata are transmitted.
**Edge deployment pattern**:
1. Agent monitors local system (logs, metrics, alerts)
2. When anomaly detected, `brain_search` queries collective for similar patterns
3. If match found, apply fix pattern from collective knowledge
4. `brain_share` contributes the resolved incident as new knowledge
5. `brain_sync` updates local LoRA weights with any new consensus
## 3. Embedding Pipeline Clarifications
### 3.1 Hash Features Are Lexical, Not Semantic
Structured hash features provide lexical and structural similarity. Texts sharing word-level n-grams will have higher cosine similarity than texts with disjoint vocabularies. **Semantics come only from the MicroLoRA transform**, which is learned from quality signals over time. A reader should not assume that hashing alone captures meaning — it captures co-occurrence structure that the LoRA refines into approximate semantic proximity.
### 3.2 Symmetric Pipeline for Store and Query
Both stored vectors and query vectors pass through the same pipeline stages in the same order:
```
text -> structured_hash_features() -> apply_lora_transform() -> L2_normalize()
```
The DentateGyrus pattern separation layer in `cognitive.rs` is used only for server-side indexing and anomaly detection. It is **not** applied to query embeddings or to stored embedding vectors used for similarity search. This prevents asymmetric drift between index and query representations.
### 3.3 Transfer Learning Boundary Conditions
Transfer priors are dampened by sqrt-scaling and require dual verification (target improved AND source not regressed). Transfers that fail verification are rejected with `TransferVerification.promotable = false`. There is no auto-apply path — every transfer requires explicit invocation and evidence.
## 4. Business Outcomes
### 4.1 Lower Cost Per Solved Task
Fewer retries and faster convergence. When a session can pull relevant prior knowledge and learned LoRA weights instead of discovering solutions from scratch, the number of API calls, tool invocations, and human review cycles decreases.
### 4.2 Higher Consistency Across Projects
Best practices become default. The MicroLoRA consensus naturally encodes the patterns that have been most successful across all contributing sessions. New projects inherit this collective "taste" from the first `brain_sync`.
### 4.3 Defensible Moat
The shared weights and witness-logged outcomes are proprietary experience, not just code. The accumulated LoRA weights represent the collective intelligence of all contributing sessions — a continuously growing asset that cannot be replicated by copying code alone.
### 4.4 Economic Accounting
```
Monthly brain value = (hours_saved x blended_hourly_rate) - cloud_costs - review_time
```
**Initial targets**:
- Reduce median fix time by 2x on "fix failing tests" workflow (30-run A/B test)
- Reduce LLM token spend per fix by 30% due to fewer retries (measured via API call counts in brain-on vs brain-off runs)
**Cloud cost estimate**: Cloud Run at 0-10 instances (2 CPU / 2Gi RAM), Firestore, GCS. Expected < $50/month at moderate traffic. Break-even requires saving approximately 1 developer-hour per month at $50/hr blended rate.
## 5. Target Users
### Primary: Developers Using Claude Code
Individual developers and small teams using Claude Code for daily software engineering tasks. The brain provides immediate value through cold-start elimination and cumulative debugging knowledge.
### Secondary: Enterprise Teams Running Always-On Agents
Teams deploying autonomous agents in production environments. The brain provides collective intelligence for incident response, configuration management, and cross-project coordination.
## 6. Access Model
### Public Read, Gated Write
- **Read**: Search, list, get, drift, partition, status, and lora/latest are publicly accessible with per-identity rate tiers (1000 reads/hr authenticated, 100 reads/hr anonymous)
- **Write**: Share, vote, delete, transfer, lora/submit require API key authentication with contributor pseudonym derivation
- **Voting is a write operation**: Quality signals are sourced only from authenticated contributors. This is a deliberate choice — public voting without identity would be the easiest Sybil/poisoning attack surface
- **Reputation gate**: New contributors start at 0.1 composite reputation. Their contributions are weighted 10x less in aggregation until they accumulate sufficient positive votes
- **System contributors**: Seed data uses `ruvector-seed` pseudonym with max reputation (1.0) and system flag exempting from decay
### Read Abuse Budgeting
Even public read endpoints can dominate cost. Defenses:
- **Query complexity limits**: Search limited to `limit <= 100`, embedding dimension fixed at 128
- **Response caching**: 60-second TTL cache on `/v1/lora/latest` (consensus changes only at epoch boundaries). 30-second cache on `/v1/status` and `/v1/drift`
- **Per-identity rate tiers**: Authenticated: 1000 reads/hr. Anonymous (no API key): 100 reads/hr. Exhausted: 429 with Retry-After header
## 7. Federation Cadence and Stability
### Aggregation Trigger
Aggregation fires on **submission count**: when `pending_submissions >= min_submissions` (default: 3). There is no time-based trigger — aggregation requires evidence from multiple independent sessions.
### Minimum Quorum
A new consensus epoch is published only when:
1. At least `min_submissions` (3) accepted submissions are pending
2. After Gate B trimming, at least `min_submissions` (3) inlier submissions remain
3. If both conditions fail, pending submissions accumulate until the threshold is met
### Rollback Semantics
When drift rollback triggers, **only MicroLoRA consensus weights are rolled back**. The accepted memory set is preserved. Memories are independently quality-gated (Bayesian BetaParams) and witness-verified — rolling them back would destroy valid, independently verified knowledge. The LoRA transform is the only aggregated learned artifact that can diverge from the population; memories are individually validated on ingestion.
After rollback, the pending submission queue is cleared to prevent the same batch from immediately re-triggering the same divergent consensus.
## 8. Acceptance Criteria
### Embedding Quality
- 200 labeled pairs (similar/unrelated)
- After 3 sync epochs with >= 10 contributors each
- Similar pairs should exceed unrelated by >= 0.25 cosine distance
- Search recall@10 should improve >= 30% vs hash-only baseline
### Workflow Acceleration
Pick one workflow (e.g., "fix failing tests in a TypeScript repo"):
- Measure median time to first passing build: brain off vs brain on
- 30 runs each condition
- Target: >= 2x improvement once the brain has a few hundred high-quality memories and a few LoRA sync epochs
### Security: Poisoning Resistance
- Zero PII leakage through embedding or LoRA weight analysis
- Byzantine aggregation excludes > 95% of adversarial submissions in poisoning simulations
- Weight drift rollback triggers correctly when consensus shifts > 5.0 L2 distance
- Poisoning simulation: 30% adversarial submitters must fail to move consensus weights beyond the drift threshold for more than one epoch
### Weekly Regression Suite
Run continuously with:
1. Fixed labeled query set (200 pairs)
2. Fixed workflow harness (e.g., "fix failing tests")
3. Poisoning simulation (30% adversarial contributors)
Pass criteria:
- Recall@10 >= 30% improvement vs hash-only baseline
- Median time to first passing build >= 2x improvement
- Adversarial consensus deviation contained to <= 1 epoch before rollback
## 9. Implementation Status
All capabilities described in this ADR are implemented on branch `feat/adr-030-hash-security-optimization`. Test counts:
| Crate | Tests | Status |
|-------|-------|--------|
| `mcp-brain` (client) | 33 | All passing |
| `mcp-brain-server` (server) | 28 | All passing |
| `mcp-brain` (doc-tests) | 1 | Passing |
| **Total** | **62** | **All passing** |
### Shipped Endpoints
| Method | Path | Category |
|--------|------|----------|
| GET | `/v1/health` | Infrastructure |
| GET | `/v1/challenge` | Auth |
| POST | `/v1/memories` | Write |
| GET | `/v1/memories/search` | Read |
| GET | `/v1/memories/list` | Read |
| GET | `/v1/memories/{id}` | Read |
| POST | `/v1/memories/{id}/vote` | Write |
| DELETE | `/v1/memories/{id}` | Write |
| POST | `/v1/transfer` | Write |
| GET | `/v1/drift` | Read |
| GET | `/v1/partition` | Read |
| GET | `/v1/status` | Read |
| GET | `/v1/lora/latest` | Read |
| POST | `/v1/lora/submit` | Write |
Total: 14 endpoints (8 read, 5 write, 1 infrastructure).
### Shipped MCP Tools
11 tools: `brain_share`, `brain_search`, `brain_get`, `brain_vote`, `brain_transfer`, `brain_drift`, `brain_partition`, `brain_list`, `brain_delete`, `brain_status`, `brain_sync`.
### Deferred
- `GET /v1/lora/stats` (per-epoch aggregation statistics) — deferred until aggregation history is implemented
- `POST /v1/lora/challenge` (proof-of-work challenge for anonymous submitters) — deferred until PoW is required by traffic patterns
## 10. RVF AGI Stack Integration (ADR-075)
The capabilities described in this ADR — PII stripping (Section 2.6, Layer 1), differential privacy (Section 2.5, Segment 8), witness chains (Section 2.5, Segment 10), and RVF container construction (Section 2.5) — were originally documented as design goals. ADR-075 implements these using the production RVF crate stack:
| Capability | Inline (Previous) | RVF Crate (ADR-075) |
|---|---|---|
| PII stripping | 8 string patterns in `verify.rs` | `rvf-federation::PiiStripper` (12 regex rules) |
| Diff privacy | Not implemented | `rvf-federation::DiffPrivacyEngine` (Gaussian, feature-gated) |
| Witness chains | String-step SHAKE-256 in `verify.rs` | `rvf-crypto::create_witness_chain` (73-byte linked entries) |
| Container build | Client-provided RVF bytes only | `rvf-wire::write_segment` (server-side construction) |
| Adversarial detect | Not implemented | `rvf-runtime::is_degenerate_distribution` (log-only) |
| Negative cache | Not implemented | `rvf-runtime::NegativeCache` (query signature blacklist) |
The 10-segment canonical container layout (Section 2.5) is now constructed server-side by the `pipeline.rs` module, with segment count reported in `ShareResponse.rvf_segments`.
## 11. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-057 | Federated RVF Transfer Learning — foundational protocol |
| ADR-058 | Hash Security Optimization — SHAKE-256 content hashing |
| ADR-059 | Shared Brain Google Cloud Deployment — infrastructure and security |
| ADR-075 | Wire Full RVF AGI Stack — replaces inline crypto with production RVF crates |

View file

@ -0,0 +1,249 @@
# ADR-061: Reasoning Kernel Architecture — Brain-Augmented Targeted Reasoning
**Status**: Accepted
**Date**: 2026-02-27
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-060 (Shared Brain Capabilities), ADR-059 (Shared Brain Google Cloud), ADR-057 (Federated Transfer Learning)
## 1. Context
The Shared Brain (ADR-059, ADR-060) provides a learning substrate: shared MicroLoRA weights, semantic retrieval, quality-gated knowledge, and witness-verified provenance. But the brain is the memory, not the thinker. The thinker is ruvllm, and it needs a reasoning kernel that leverages the brain to achieve "on par or better" performance compared to traditional LLMs on targeted domains.
A traditional LLM is a static generalist with weak memory and no accountability. The RuVector system is a living specialist with a shared skill substrate, a semantic retrieval layer, a policy and proof layer, and a parameter growth path. That combination beats raw model size in production for bounded domains.
This ADR defines the reasoning kernel architecture: what the brain can and cannot do, how ruvllm grows through the brain, and the concrete build plan for targeted domain reasoning.
## 2. What the Brain Can and Cannot Do
### 2.1 What It Does Well
**Domain reasoning at high reliability**: When the domain is bounded — codebase-specific debugging, infrastructure incident response, RVF correctness, mincut-based coherence decisions — the brain makes the system consistently better than a generic LLM because it accumulates exact successful trajectories and policies.
**Fast improvement without retraining**: MicroLoRA plus retrieval shifts behavior quickly. Short-cycle learning driven by votes and outcomes. A 2KB weight sync moves faster than any model upgrade.
**Better behavior under governance**: Witness logs, proof-gated mutation, drift monitors, and rollbacks give controlled reasoning. That often matters more than raw model IQ in production.
### 2.2 What It Cannot Do
**Create novel general reasoning from scratch**: LoRA adapts a model. It does not invent capacity absent in the base network. If ruvllm is small and weak at general reasoning, MicroLoRA and retrieval help substantially, but there is a ceiling.
**Replace a large model on broad world knowledge**: The brain stores distilled artifacts, not the full distribution of human text. The system wins by focus, not by universality.
## 3. Reasoning Kernel Design
### 3.1 Reasoning Kernel Definition
The reasoning kernel is the fixed protocol by which ruvllm processes a task using the brain.
**Input**: A task type from a bounded set (initially: fix failing tests, explain ADR contradictions, generate RVF segments, perform mincut-gated decisions).
**Output**: A fixed action format with tool calls, intermediate checks, and a final answer format.
**Invariant**: Every reasoning step produces a witnessable artifact — a check result, a test pass, a proof token, or a retrieval citation from the brain. This makes reasoning verifiable and replayable.
### 3.2 Memory Architecture
The kernel operates with two memory layers sourced from the brain:
**Working memory** (per-task, ephemeral):
- RAG into the current context window using `brain_search`
- Local repo context (files, errors, stack traces)
- Retrieved similar trajectories with quality scores
**Skill memory** (cross-session, persistent):
- High-quality patterns from `brain_search(min_quality: 0.7)`
- PolicyKernel snapshots that bias the plan generator
- CostCurve data that predicts acceleration factors
- MicroLoRA consensus weights that transform embeddings toward effective solution regions
### 3.3 Reasoning Loop
```
1. CLASSIFY task type from bounded set
2. PULL working memory: brain_search(query, category, min_quality)
3. PULL skill memory: brain_sync(direction: "pull") for MicroLoRA consensus
4. PLAN using retrieved patterns + local context
5. EXECUTE with tool calls, recording each step as witnessable artifact
6. VERIFY against success criteria (test pass, proof token, etc.)
7. If success:
a. brain_share(category, result) — contribute solution
b. brain_vote(id, "up") on retrieved patterns that helped
c. brain_sync(direction: "push") — export local LoRA deltas
8. If failure:
a. brain_vote(id, "down") on retrieved patterns that misled
b. Record failure trajectory for negative example mining
```
## 4. Training Pipeline — Growing ruvllm Through Brain Data
The brain accumulates exactly the data types needed for targeted model improvement. Training happens in three layers.
### 4.1 Layer A: Preference Learning from Votes
**Data source**: `brain_vote` events produce (memory, direction) pairs. Over time, this builds a preference dataset: for a given task type, which solution patterns are preferred (upvoted) vs rejected (downvoted).
**Training method**: Direct Preference Optimization (DPO) or reward model training on the targeted model. The Bayesian quality scores (`BetaParams`) provide confidence-weighted preference signals — high-observation memories with clear quality separation are stronger training signals than low-observation ones.
**Frequency**: Batch, triggered when accumulated vote pairs exceed a threshold (e.g., 500 new preference pairs per domain).
### 4.2 Layer B: Imitation Learning from Successful Trajectories
**Data source**: SONA trajectories with quality > 0.7, exported via `AgentExport` from `sona::training::federated`. Each trajectory includes the sequence of tool calls, intermediate states, and final outcome.
**Training method**: Supervised fine-tuning on high-quality trajectories. Narrow and task-specific — only trajectories from the target domain (e.g., "debug" category memories with quality > 0.7).
**Curriculum**: Use `brain_partition` clusters to group trajectories by difficulty. Train easy clusters first, hard clusters later (curriculum learning).
### 4.3 Layer C: Continual Learning with Forgetting Control
**Data source**: All layers A and B, plus replay buffers keyed by domain and mincut partition.
**Training method**: Elastic Weight Consolidation (EWC) with lambda=2000.0 (matching SONA's existing EWC configuration). The Fisher information matrix is computed per-domain partition, so the model preserves capabilities in well-established domains while adapting to new ones.
**Replay buffer design**: Each mincut partition maintains a fixed-size replay buffer (100 exemplars). When a new exemplar is added, the lowest-quality one is evicted. This prevents catastrophic forgetting on older domains while allowing new domains to develop.
### 4.4 Training Data Flow
```
Brain Events Training Pipeline
----------- ------------------
brain_vote(up/down) --> Layer A: Preference pairs
brain_share(quality>0.7) --> Layer B: Imitation trajectories
brain_partition clusters --> Layer C: EWC domains + replay buffers
brain_sync (LoRA deltas) --> Continuous adapter refinement
```
## 5. Parameter Growth Strategy
Add capacity only when a measured ceiling is reached. If the model cannot solve a class of tasks even with perfect retrieval and high-quality patterns, then expand.
### 5.1 Growth Decision Criteria
Trigger parameter growth when:
- Success rate on a domain benchmark plateaus for 3+ epochs despite growing memory
- Retrieval recall@10 is > 80% (the right knowledge is available) but task success < 60% (the model cannot use it)
- MicroLoRA weight norms are saturating (approaching clipping bounds consistently)
### 5.2 Growth Options (Ordered by Cost)
**Option 1: Expand adapter rank** (cheapest)
Increase MicroLoRA rank from 2 to 4 or 8 for specific layers. Keep base frozen. This quadruples expressiveness for 4x parameter cost (still < 10KB per sync).
**Option 2: Add specialist heads per domain partition** (moderate)
Mixture-of-experts routing using mincut partitions as the router signal. Each partition gets a small specialist head. Base model shared, routing learned from partition assignments.
**Option 3: Train a larger ruvllm tier and distill** (expensive)
Use the brain to provide curriculum and hard negative examples. Distill from the larger model back to the smaller one, keeping the brain-augmented inference path.
## 6. Evaluation Framework
### 6.1 Metrics
| Metric | Definition | Target |
|--------|-----------|--------|
| **Quality** | Success rate on fixed benchmark per domain | >= 80% on target domain |
| **Efficiency** | Median tokens + tool calls per solved task | >= 30% reduction vs baseline |
| **Reliability** | Variance reduction across epochs | Std dev < 0.5x baseline |
| **Safety** | Poisoning simulation behavior deviation | < 5% regression under 30% adversarial |
### 6.2 Three-System Comparison
For each target domain, compare:
1. **Baseline ruvllm**: No brain, no retrieval, no LoRA
2. **ruvllm + brain retrieval**: brain_search for working memory, no LoRA
3. **ruvllm + brain retrieval + MicroLoRA**: Full pipeline with federated weights
Pass criteria:
- System 3 achieves >= 20% higher success rate than System 1
- System 3 achieves >= 30% fewer tokens per success than System 1
- Regressions held under 5% across 3 consecutive sync epochs
### 6.3 Initial Benchmark: Fix Failing Tests
**Domain**: Rust crate test failures across the RuVector monorepo.
**Benchmark size**: 100 items (real test failures extracted from CI history).
**Task format**: Given a failing test and its error output, produce a fix that makes the test pass.
**Success criteria**: The fix compiles and the specific test passes. No regressions in other tests.
**Why this domain first**:
- Highest frequency task in daily development
- Binary success signal (test passes or does not)
- Bounded — Rust compiler errors are structured and classifiable
- Rich trajectory data from existing sessions
- Direct impact on developer velocity
## 7. Hybrid Lane Architecture
ruvllm is offline-capable by default. The brain is an optional accelerator, not a dependency.
### 7.1 Degradation Path
| Connectivity | Available Features | Performance |
|-------------|-------------------|-------------|
| **Full** (brain reachable) | All 11 MCP tools + MicroLoRA sync + retrieval | Best |
| **Partial** (brain slow/intermittent) | Local SONA + cached consensus LoRA + hash features | Good |
| **Offline** (no brain) | Local SONA + hash-only features | Baseline |
The `BrainEmbedder` already implements this degradation:
- If consensus LoRA cached: use it (no network needed)
- If SONA engine available: use local LoRA from SONA
- Otherwise: structured hash features only
### 7.2 Sync Cadence
`brain_sync` is explicit opt-in, not automatic. Recommended cadence:
- **On session start**: `brain_sync(direction: "pull")` — get latest consensus
- **On session end**: `brain_sync(direction: "push")` — export local learning
- **Periodically during long sessions**: Every ~100 embeddings processed
## 8. Implementation Status
### Implemented (in this branch)
| Component | Status |
|-----------|--------|
| Structured hash features (Stage 1 embedding) | Shipped, 6 new tests |
| MicroLoRA forward pass (Stage 2 embedding) | Shipped |
| Consensus import/export (`BrainEmbedder`) | Shipped |
| `GET /v1/lora/latest` | Shipped |
| `POST /v1/lora/submit` | Shipped |
| Gate A policy validation | Shipped |
| Gate B robust aggregation (median + MAD + reputation) | Shipped |
| Consensus drift monitoring + rollback | Shipped |
| `brain_sync` MCP tool | Shipped |
| Hybrid degradation path | Shipped |
### Deferred (future work)
| Component | Priority | Dependency |
|-----------|----------|------------|
| Training data export endpoints | High | Need trajectory format spec |
| Preference pair extraction from votes | High | Need DPO training loop |
| Trajectory quality filtering | Medium | Need benchmark suite |
| EWC per-partition Fisher matrices | Medium | Need partition stability |
| Specialist head routing | Low | Need ceiling measurement |
| 100-item benchmark harness | High | Need CI history extraction |
## 9. Answers to Design Questions
**Q: What is the narrowest set of tasks to beat a traditional LLM on first?**
A: Code debugging — specifically, fix failing tests in Rust crates. Highest frequency, clearest success signal, bounded domain, rich trajectory data already available.
**Q: Should ruvllm remain fully offline capable by default?**
A: Yes, with a hybrid lane for acceleration. The `BrainEmbedder` degradation path (consensus LoRA -> local SONA -> hash-only) ensures offline functionality. The brain is an accelerator, not a dependency.
## 10. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-057 | Federated RVF Transfer Learning — protocol foundation |
| ADR-058 | Hash Security Optimization — SHAKE-256 for content integrity |
| ADR-059 | Shared Brain Google Cloud — infrastructure and security |
| ADR-060 | Shared Brain Capabilities — sub-capabilities and business outcomes |

View file

@ -0,0 +1,357 @@
# ADR-062: Brainpedia — Structured Knowledge Encyclopedia with Delta-Based Editing
**Status**: Accepted
**Date**: 2026-02-27
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-061 (Reasoning Kernel Architecture)
## 1. Context
The Shared Brain (ADR-059, ADR-060) accumulates knowledge as individual memories — embeddings, quality scores, witness chains. But memories are granular artifacts, not structured knowledge. A developer looking for "how does RVF serialization work" may find 30 loosely related memories with no coherent narrative.
Wikipedia solves this for human text. The Brainpedia applies the same concept — canonical, community-maintained knowledge pages — but the unit of contribution is not raw text. It is a structured, versioned, verifiable knowledge artifact with outcomes.
Every entry is an RVF memory plus a delta stream. Contributions are deltas, not overwrites. Evidence links tie claims to outcome proofs. Transfer assets make knowledge portable across domains. The result is a knowledge encyclopedia that is executable, learning-native, transferable, and reversible.
## 2. Design Principles
### 2.1 The Unit of Contribution
A Brainpedia contribution is one of:
**RVF Memory**: A canonical knowledge artifact with the standard 10-segment layout (ADR-060 Section 2.5). This is the stable snapshot — the "current page version."
**Delta Entry**: A structured modification to an existing page. Deltas carry their own witness chain and are individually verifiable. A delta contains:
- `parent_id`: The memory ID being modified
- `delta_type`: One of `Correction`, `Extension`, `Evidence`, `Deprecation`
- `content_diff`: The structured change (not raw text diff — semantic field updates)
- `evidence_links`: Outcome proofs that support the change
- `contributor_id`: Pseudonym of the contributor
- `witness_hash`: SHAKE-256 chain entry for this delta
**Evidence Link**: A reference to a verifiable outcome that supports a claim. Evidence types:
- `TestPass { test_name, repo, commit_hash }`: A test that passed after applying the knowledge
- `BuildSuccess { pipeline_url, commit_hash }`: A CI build that succeeded
- `MetricImproval { metric_name, before, after, measurement_url }`: A measured improvement
- `PeerReview { reviewer_pseudonym, vote_direction, quality_score }`: A peer attestation
### 2.2 Properties of Knowledge Entries
Every Brainpedia entry is:
**Executable**: Contains embeddings, transfer priors, and policy kernels that can be directly consumed by `brain_search`, `brain_sync`, and `brain_transfer`. Knowledge is not just readable — it is machine-consumable.
**Learning-native**: Quality scores update via Bayesian voting (BetaParams). High-quality entries strengthen the MicroLoRA consensus. Low-quality entries are auto-archived. The encyclopedia improves through use.
**Transferable**: Transfer assets (TransferPrior, PolicyKernel, CostCurve) enable cross-domain knowledge portability. A debugging pattern from Rust can transfer to TypeScript with measured acceleration.
**Reversible**: No direct mutation. Every change is a delta. The full history is reconstructable from the delta stream. Any delta can be reverted by applying a compensating delta. Witness chains prove the order and integrity of all changes.
## 3. Page Structure
A Brainpedia page is a canonical memory plus its delta log.
### 3.1 Canonical Page
The canonical page is a `BrainMemory` with `page_status: Canonical`. It represents the current community-accepted version of the knowledge. Canonical pages have:
- `category`: The knowledge domain (Architecture, Pattern, Solution, Debug, etc.)
- `title`: Human-readable page title (unique within category)
- `content`: The structured knowledge content
- `tags`: Searchable labels
- `embedding`: Structured hash features + MicroLoRA transform (standard pipeline)
- `quality_score`: Bayesian BetaParams accumulated from votes
- `evidence_count`: Number of verified evidence links
- `delta_count`: Number of applied deltas
- `transfer_assets`: Optional TransferPrior + PolicyKernel + CostCurve
### 3.2 Delta Log
Each page maintains an ordered delta log. Deltas are immutable once accepted. The delta log enables:
- **Version reconstruction**: Any historical version can be reconstructed by replaying deltas
- **Attribution**: Every change is attributed to a specific contributor
- **Audit**: Witness chains prove the integrity of the delta sequence
- **Revert**: A `Deprecation` delta marks a previous delta as superseded
### 3.3 Evidence Links
Evidence links are the mechanism by which the encyclopedia distinguishes claims from knowledge. A claim without evidence is a candidate. A claim with evidence is knowledge.
**Evidence lifecycle**:
1. Contributor submits a delta with one or more evidence links
2. Server verifies evidence link format and contributor authentication
3. Evidence is recorded in the delta's witness chain
4. Other contributors can add corroborating evidence via additional deltas
5. Quality score reflects the evidence density: more verified evidence = higher quality
## 4. Governance Gates
Three gates control what enters the encyclopedia and how.
### 4.1 Gate 1: Identity
All contributions require API key authentication with contributor pseudonym derivation (same as ADR-060 Section 6). Anonymous users can read but cannot contribute. This prevents Sybil attacks on the knowledge base.
### 4.2 Gate 2: Evidence
**For deltas**: Every delta must include at least one evidence link. A correction without evidence is rejected. An extension without evidence is held in a `Proposed` state until evidence is added.
**For new pages**: A new canonical page requires at least 3 evidence links from independent contributors (not the page creator) before promotion from `Draft` to `Canonical`. This ensures community validation before knowledge becomes authoritative.
**Evidence verification**: The server does not execute tests or check CI — it verifies that evidence links are well-formed, that the contributor is authenticated, and that the linked artifacts exist (where checkable). Outcome verification is a community responsibility: voters assess whether evidence is credible.
### 4.3 Gate 3: Consensus
**Page promotion**: A Draft page becomes Canonical when:
- `quality_score.mean() >= 0.7` (community approval)
- `quality_score.observations() >= 5` (sufficient review)
- `evidence_count >= 3` from `>= 2` distinct contributors
**Delta acceptance**: A delta is accepted when:
- Contributor is authenticated (Gate 1)
- At least one evidence link is provided (Gate 2)
- Contributor has not been flagged for poisoning (reputation > 0.1)
**Delta promotion to canonical**: When a delta accumulates sufficient quality votes, the canonical page is updated to incorporate the delta. The old canonical version is preserved in the delta log.
## 5. Submission Model
### 5.1 New Users (Reputation < 0.5, Contribution Count < 10)
New users can:
- Read all pages and delta logs
- Submit deltas to existing pages (with evidence)
- Vote on pages and deltas
- Cannot create new canonical pages
This prevents low-reputation users from flooding the encyclopedia with low-quality pages while still allowing them to contribute improvements to existing knowledge.
### 5.2 Established Users (Reputation >= 0.5, Contribution Count >= 10)
Established users can:
- All new user capabilities
- Create new pages (initially as `Draft`)
- Promote deltas with sufficient evidence
### 5.3 System Contributors
System contributors (`is_system: true`, e.g., `ruvector-seed`) can:
- All established user capabilities
- Create pages directly as `Canonical` (for seed data)
- Bypass evidence count requirements (seed data is pre-validated)
## 6. Delta-Based Editing Protocol
### 6.1 No Direct Mutation
The Brainpedia has no "edit page" operation. All modifications are deltas. This is enforced at the API level — the `PUT /v1/pages/{id}` endpoint does not exist.
### 6.2 Delta Types
**Correction**: Fixes an error in the canonical page. Requires evidence showing the current content is wrong and the correction is right.
**Extension**: Adds new information to the canonical page. Requires evidence showing the extension is valid and useful.
**Evidence**: Adds a new evidence link to an existing claim. No content change — purely strengthens the evidentiary basis.
**Deprecation**: Marks the canonical page or a specific delta as superseded. Requires evidence showing why the content is no longer valid (e.g., a library version change, a security vulnerability discovered).
### 6.3 Conflict Resolution
When multiple deltas target the same section of a canonical page, conflict resolution uses:
1. **Quality score**: Higher-quality deltas take precedence
2. **Evidence density**: More evidence wins ties
3. **Recency**: Among equal quality and evidence, newer deltas win
4. **Manual resolution**: If automated resolution fails, the page enters `Contested` status and requires a contributor with reputation >= 0.7 to resolve
## 7. First Domain: Code Debugging
The first domain for the Brainpedia is **code debugging** (category: `Debug`). Rationale (same as ADR-061 Section 6.3):
- Highest frequency task in daily development
- Binary success signal (test passes or does not)
- Bounded domain — compiler errors are structured and classifiable
- Rich trajectory data from existing sessions
- Direct impact on developer velocity
### 7.1 Initial Page Structure for Debug Domain
Each debug page covers a specific failure class:
**Title**: Descriptive name for the failure pattern (e.g., "tokio::spawn deadlock with parking_lot::RwLock")
**Content**: Structured fields:
- `error_pattern`: The error message or symptom signature
- `root_cause`: Why this failure occurs
- `fix_pattern`: The canonical fix approach
- `code_before`: Example code that triggers the failure
- `code_after`: Example code after the fix
- `applies_to`: Language, framework, and version constraints
**Evidence**: TestPass links showing the fix resolves the error in specific repositories.
**Transfer assets**: TransferPrior for similar concurrency patterns in other async runtimes.
### 7.2 Seed Pages
The seed pipeline (`ruvector-seed` contributor) will create initial debug pages from:
- Known Rust compiler error patterns
- Common `tokio` / `async-std` pitfalls
- RVF serialization edge cases
- MinCut graph construction errors
- SONA learning convergence failures
## 8. Data Model Extensions
### 8.1 New Types
```rust
/// Page status in the Brainpedia lifecycle
pub enum PageStatus {
Draft, // Newly created, awaiting evidence and votes
Canonical, // Community-accepted, authoritative
Contested, // Conflicting deltas, needs resolution
Archived, // Superseded or low-quality, read-only
}
/// A delta entry modifying a canonical page
pub struct PageDelta {
pub id: Uuid,
pub page_id: Uuid, // The canonical page being modified
pub delta_type: DeltaType,
pub content_diff: serde_json::Value, // Structured field updates
pub evidence_links: Vec<EvidenceLink>,
pub contributor_id: String,
pub quality_score: BetaParams,
pub witness_hash: String,
pub created_at: DateTime<Utc>,
}
pub enum DeltaType {
Correction,
Extension,
Evidence,
Deprecation,
}
/// Evidence linking a claim to a verifiable outcome
pub struct EvidenceLink {
pub evidence_type: EvidenceType,
pub description: String,
pub contributor_id: String,
pub verified: bool, // Server-side format check passed
pub created_at: DateTime<Utc>,
}
pub enum EvidenceType {
TestPass { test_name: String, repo: String, commit_hash: String },
BuildSuccess { pipeline_url: String, commit_hash: String },
MetricImproval { metric_name: String, before: f64, after: f64 },
PeerReview { reviewer: String, direction: VoteDirection, score: f64 },
}
```
### 8.2 Extended BrainMemory
The existing `BrainMemory` gains optional page metadata:
```rust
// Added to BrainMemory
pub page_status: Option<PageStatus>,
pub evidence_count: u32,
pub delta_count: u32,
```
### 8.3 New API Endpoints
| Method | Path | Purpose | Gate |
|--------|------|---------|------|
| POST | `/v1/pages` | Create a new Draft page | Identity + Reputation >= 0.5 |
| GET | `/v1/pages/{id}` | Get page with delta log | Public read |
| POST | `/v1/pages/{id}/deltas` | Submit a delta | Identity + Evidence |
| GET | `/v1/pages/{id}/deltas` | List deltas for a page | Public read |
| POST | `/v1/pages/{id}/evidence` | Add evidence to a page | Identity |
| POST | `/v1/pages/{id}/promote` | Promote Draft to Canonical | Consensus (auto-checked) |
### 8.4 New MCP Tools
| Tool | Purpose |
|------|---------|
| `brain_page_create` | Create a new knowledge page (Draft) |
| `brain_page_get` | Get a page with its delta log and evidence |
| `brain_page_delta` | Submit a delta to an existing page |
| `brain_page_evidence` | Add evidence to a page or delta |
| `brain_page_search` | Search pages by title, content, or category |
| `brain_page_list` | List pages by category, status, or quality |
## 9. Implementation Status
### Deferred (Future Work)
All components described in this ADR are deferred for future implementation. They build on the existing Shared Brain infrastructure (ADR-059, ADR-060) which is fully implemented.
| Component | Priority | Dependency |
|-----------|----------|------------|
| `PageStatus` enum + `PageDelta` types | High | None (types only) |
| Delta submission endpoint | High | Types |
| Evidence link validation | High | Delta endpoint |
| Page creation with reputation gate | Medium | Reputation system (shipped) |
| Page promotion logic | Medium | Evidence + quality thresholds |
| Conflict resolution | Low | Multiple concurrent deltas |
| Seed debug pages | High | Page creation endpoint |
| MCP tools (6 new) | Medium | All endpoints |
## 10. Acceptance Criteria
### Evidence-Gated Quality
- 50 debug pages seeded from known Rust error patterns
- Each page requires >= 3 evidence links before Canonical promotion
- Pages without evidence remain Draft and are excluded from `brain_search` top-k results
- Quality score distribution: Canonical pages have mean quality >= 0.7
### Delta Integrity
- Full delta log reconstructs any historical page version
- Every delta has a witness chain entry verifiable by any participant
- Deprecation deltas correctly mark superseded content
- No direct mutation path exists (no PUT endpoint on page content)
### Reputation Gating
- New users (reputation < 0.5) cannot create pages (403 response)
- New users can submit deltas with evidence (accepted)
- System contributors can create Canonical pages directly
- Poisoned contributor (reputation < 0.1) cannot submit deltas (403 response)
### First Domain Validation
Run on the Debug category:
- 50 seeded debug pages covering common Rust error patterns
- 10 community-contributed deltas with TestPass evidence
- 5 pages promoted from Draft to Canonical via consensus
- Mean search recall@10 on debug queries >= 40% improvement vs unstructured memories
## 11. Answers to Design Questions
**Q: What is the first domain you want the community Brainpedia to cover?**
A: Code debugging. Same rationale as ADR-061: highest frequency task, binary success signal, bounded domain, richest existing trajectory data. Debug pages have natural evidence links (TestPass, BuildSuccess) that make the evidence gate practical from day one.
**Q: Do you want anyone to be able to submit pages, or only submit deltas to existing pages until they earn reputation?**
A: Deltas only until reputation is earned. New users (composite reputation < 0.5, contribution count < 10) can submit deltas to existing pages but cannot create new pages. This prevents low-reputation flooding while encouraging incremental improvement. Page creation requires demonstrated quality through delta contributions.
## 12. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-057 | Federated RVF Transfer Learning — protocol foundation |
| ADR-058 | Hash Security Optimization — SHAKE-256 for content integrity |
| ADR-059 | Shared Brain Google Cloud — infrastructure and security |
| ADR-060 | Shared Brain Capabilities — sub-capabilities and business outcomes |
| ADR-061 | Reasoning Kernel Architecture — training pipeline that consumes Brainpedia data |

View file

@ -0,0 +1,399 @@
# ADR-063: WASM Executable Nodes — Deterministic Compute at the Edge
**Status**: Accepted
**Date**: 2026-02-27
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-062 (Brainpedia Architecture)
## 1. Context
The Shared Brain stores knowledge as embeddings and serves MicroLoRA weights. But the embedding pipeline — structured hash features, LoRA transform, L2 normalization — runs only on the server and in the local MCP client. Browser users, edge appliances, and third-party integrations cannot produce compatible embeddings without reimplementing the pipeline.
WASM solves this. A signed WASM module that implements feature extraction can run identically in the browser, on the appliance, and on the server. The same bytecode, the same results, cryptographically verified.
This ADR defines the WASM node architecture: how nodes are built, signed, served, executed, and integrated with the Shared Brain and MicroLoRA pipeline.
## 2. What WASM Nodes Enable
### 2.1 Executable Knowledge Pages
A Brainpedia entry can include a small WASM module that validates, scores, normalizes, or transforms content locally. Knowledge becomes executable, not just readable.
### 2.2 Deterministic Verification
The same scoring and feature extraction runs in browser, on the appliance, and on the server with identical results. No "works on my machine" for embeddings.
### 2.3 Safer Public Compute
WASM with capability gating lets untrusted users run brain logic without server-side code execution. Pure compute only — no filesystem, no network, no clock access.
## 3. Node Architecture
### 3.1 WASM Segment Type
WASM nodes are signed RVF artifacts. They use the existing RVF segment layout with a new segment type for WASM bytecode.
A node RVF container contains:
- `MANIFEST (0x05)`: Segment directory
- `META_SEG (0x07)`: Node name, version, description, interface schema
- `WASM_SEG (0x40)`: WASM bytecode (compiled, not WAT)
- `WITNESS_SEG (0x0A)`: SHAKE-256 hash chain (build → sign → publish)
- `CRYPTO_SEG (0x0C)`: Ed25519 signature over all segments
### 3.2 Hash Choice Consistency
Two hash functions serve distinct purposes:
**SHA-256** for HTTP-layer integrity: `X-Node-SHA256` header, `wasm_sha256` in signed manifests, GCS content-addressed storage paths, browser cache keys. SHA-256 is conventional for HTTP caching and CDN infrastructure.
**SHAKE-256** for RVF witness chains: all WITNESS_SEG entries use SHAKE-256 to maintain consistency with the global RVF witness chain convention (ADR-058). Witness chains are internal to the RVF container format, not exposed via HTTP.
These do not conflict. SHA-256 is the identity hash visible to HTTP clients. SHAKE-256 is the provenance hash visible to RVF verifiers.
### 3.3 V1 ABI Specification
The v1 ABI is `feature_extract` only. `score` and `validate` are deferred to v2 after the determinism guarantee is proven on the simpler single-function interface. Keeping v1 to one function reduces the ABI surface that must be verified across three runtimes.
**ABI version**: `1`
**Required exports**:
- `memory`: Linear memory export (must declare a maximum size, rejected if absent or > 16 pages)
- `malloc(size: i32) -> i32`: Allocate memory for input
- `feature_extract_dim() -> i32`: Return the output dimension (must be constant, e.g., 128)
- `feature_extract(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32`: Extract features from text at `in_ptr`/`in_len`, write f32 vector to `out_ptr`, return dimension written
**Calling convention**:
1. Caller invokes `feature_extract_dim()` to learn output size
2. Caller allocates input buffer via `malloc(input_bytes_len)`
3. Caller writes UTF-8 text into the allocated buffer
4. Caller allocates output buffer via `malloc(dim * 4)` (f32 = 4 bytes)
5. Caller invokes `feature_extract(in_ptr, in_len, out_ptr)` → returns `dim`
6. Caller reads `dim` f32 values directly from `memory` at `out_ptr`
No global result state. No `read_f32` indirection. The caller owns both input and output pointers. This is the fastest path in both browser and wasmtime.
**Embedded lookup tables**: Nodes MAY include constant data (token maps, n-gram tables, normalization tables) compiled into the WASM data section. This is fine — the data section is part of the signed bytecode and does not violate the pure-compute constraint. The data section is immutable at runtime.
### 3.4 Determinism Constraints
"Bit-identical" is a strong claim. These constraints make it true in practice:
1. **No f64 in v1 nodes**: All arithmetic uses f32 only. f64 intermediate results are prohibited because fused multiply-add behavior varies across engines for f64.
2. **No SIMD for v1 nodes**: The WASM module must not use the SIMD proposal. SIMD instruction selection and NaN propagation differ across engines.
3. **Deterministic compilation flags**: Nodes must be compiled with `no-fast-math` (Rust: `-C target-feature=-fast-math`, or simply do not opt in). No `-ffast-math`, no `reassociate`, no `reciprocal`.
4. **NaN scrubbing before output**: The last operation before writing each f32 to the output buffer must check for NaN and replace with 0.0f32. This prevents non-deterministic NaN propagation from producing environment-dependent results.
5. **Rounding mode**: IEEE 754 round-to-nearest-even (the WASM default). No explicit rounding mode changes.
These constraints are verified at publish time where possible (SIMD detection, f64 usage detection via module inspection) and at runtime via conformance test vectors.
### 3.5 Capability Model
**V1: zero imports**. The WASM module's import section must be empty. No `env`, no WASI, no custom imports. If the import section is non-empty, the module is rejected at publish time. This is the cleanest rule for pure compute.
If future nodes require host capabilities (v2+), they are served from a separate endpoint (`/v1/nodes/privileged/`) with explicit capability declarations. Privileged nodes require reputation >= 0.7 to publish.
### 3.6 Resource Limits
Resource limits are defined differently per environment because browser Workers lack instruction-level metering.
**Server and appliance (wasmtime)**:
- Max fuel: calibrated to 25ms on reference CPU (equivalent to ~100M simple instructions)
- Max memory: module must declare maximum <= 16 pages (1MB)
- Epoch interruption as backstop
**Browser (WebAssembly.instantiate in Worker)**:
- Max wall clock: 25ms (Worker `setTimeout` termination)
- Max input size: 8KB UTF-8
- Worker isolation: `postMessage` only, no DOM, no fetch, no storage
**All environments**:
- Max WASM binary size: 1MB
- Max input size: 8KB UTF-8
The 25ms budget is aligned across environments. Server fuel is calibrated against reference CPU to approximate 25ms of compute.
## 4. Signing and Verification
### 4.1 Canonical Signed Manifest
The Ed25519 signature covers a canonical binary manifest, not raw JSON. This prevents serialization differences from breaking verification.
**Signed manifest fields** (fixed-order binary encoding):
| Field | Type | Bytes |
|-------|------|-------|
| `abi_version` | u8 | 1 |
| `id_len` | u16 LE | 2 |
| `id` | UTF-8 | variable |
| `version_len` | u16 LE | 2 |
| `version` | UTF-8 | variable |
| `dim` | u16 LE | 2 |
| `wasm_sha256` | [u8; 32] | 32 |
| `created_at` | i64 LE (Unix epoch seconds) | 8 |
| `allowed_imports_hash` | [u8; 32] | 32 (SHA-256 of empty string for v1) |
| `compiler_tag_len` | u16 LE | 2 |
| `compiler_tag` | UTF-8 | variable (e.g., "rustc-1.77-wasm32") |
The signature is `Ed25519.sign(signing_key, manifest_bytes)`. Verification is `Ed25519.verify(public_key, manifest_bytes, signature)`.
This encoding is deterministic: fixed field order, little-endian integers, explicit length prefixes. No JSON, no CBOR, no ambiguity.
### 4.2 Verification Flow
1. Client fetches node metadata (`GET /v1/nodes/{id}/{version}`)
2. Client fetches WASM binary (`GET /v1/nodes/{id}/{version}/node.wasm`)
3. Client computes SHA-256 of WASM bytes, compares to `wasm_sha256` in metadata
4. Client reconstructs manifest bytes from metadata fields
5. Client verifies Ed25519 signature over manifest bytes
6. Client runs conformance test vectors (Section 5)
7. If all pass: instantiate and use. If any fail: reject.
## 5. Conformance Test Vectors
Conformance test vectors are first-class policy, not optional documentation.
### 5.1 Test Vector Format
Every node's metadata includes a `conformance` object:
```json
{
"abi_version": 1,
"dim": 128,
"test_vectors": [
{
"input": "deadlock tokio spawn rwlock",
"expected_output_sha256": "a1b2c3d4..."
},
{
"input": "",
"expected_output_sha256": "e5f6a7b8..."
},
{
"input": "日本語テスト Unicode edge case",
"expected_output_sha256": "c9d0e1f2..."
}
]
}
```
**20 mandatory test vectors** including:
- Empty string
- Single character
- ASCII only
- Unicode (CJK, emoji, combining characters, RTL)
- Maximum length (8KB)
- Repeated tokens
- Whitespace-only
- Mixed case sensitivity test pairs
### 5.2 Verification Method
For each test vector:
1. Run `feature_extract` with the input text
2. Read the output f32 vector as raw bytes (little-endian)
3. Compute SHA-256 of the raw output bytes
4. Compare to `expected_output_sha256`
**Output byte hash, not float comparison.** Float printing can hide differences. Raw byte SHA-256 catches any bit-level divergence.
### 5.3 Runtime Self-Test
Every runtime (browser, server, appliance) runs all conformance vectors on first load of a node. If any vector fails, the node is rejected and not used. The server logs conformance failures as security events.
## 6. Endpoints
### 6.1 Node Registry
| Method | Path | Purpose | Auth |
|--------|------|---------|------|
| GET | `/v1/nodes` | List available nodes | Public |
| GET | `/v1/nodes/{id}/{version}` | Get node metadata + conformance vectors | Public |
| GET | `/v1/nodes/{id}/{version}/node.wasm` | Download WASM binary | Public |
| POST | `/v1/nodes` | Publish a new node | Identity + Reputation >= 0.5 |
| DELETE | `/v1/nodes/{id}/{version}` | Revoke a node (original publisher only) | Identity |
### 6.2 Version Resolution
There is no `GET /v1/nodes/{id}.wasm` (no implicit "latest"). Clients must specify both `id` and `version`. This prevents supply-chain attacks where a compromised node replaces a trusted version.
The `GET /v1/nodes` listing includes all versions of all nodes. Clients filter by `id` and select the desired version.
### 6.3 Response Headers
**GET /v1/nodes/{id}/{version}/node.wasm**:
- `Content-Type: application/wasm`
- `Cache-Control: public, immutable, max-age=31536000`
- `X-Node-SHA256: {hex-encoded SHA-256 of WASM bytes}`
Immutable caching is safe because the version is in the path. New versions get new paths.
### 6.4 Revocation
`DELETE /v1/nodes/{id}/{version}` marks a node as revoked:
- Only the original publisher can revoke
- Revocation writes an audit record to the witness chain
- WASM bytes remain in storage (for forensic analysis), but the registry stops serving the node
- `GET /v1/nodes/{id}/{version}` returns 410 Gone with a revocation reason
- `GET /v1/nodes/{id}/{version}/node.wasm` returns 410 Gone
### 6.5 Content-Addressed Storage
GCS stores WASM binaries by content hash, not by ID:
```
gs://ruvector-brain-{region}/nodes/sha256/{hash}.wasm
```
The registry maps `(id, version)` → `hash`. Multiple nodes referencing the same bytecode share storage. This makes bytes immutable at the storage layer — the registry controls visibility, not the blob store.
## 7. Integration with Shared Brain
### 7.1 Embedding Pipeline in Browser
The clean split for browser-side semantic search:
1. **WASM node** (`sona_feature/1.0.0`): runs `feature_extract()` to produce structured hash features (128-dim f32 vector)
2. **Brain API** (`GET /v1/lora/latest`): browser fetches current MicroLoRA weights (2KB JSON)
3. **JS or second WASM node**: applies MicroLoRA transform `output = L2_norm(features + scale * (features @ down) @ up)`
4. **Brain API** (`GET /v1/memories/search`): browser sends the transformed embedding for server-side ranked search
This gives semantic retrieval in the browser with no heavy model download.
### 7.2 Brainpedia Integration
Brainpedia pages (ADR-062) can reference WASM nodes:
- A debug page can link to a `feature_extract` node for domain-specific embedding
- Evidence links can include deterministic WASM validation results
- Pages declare which node version they were indexed with for reproducibility
### 7.3 Appliance Mode
Edge appliances run the same WASM nodes as the browser and server:
1. Load node from local cache or `GET /v1/nodes/{id}/{version}/node.wasm`
2. Run conformance self-test (Section 5.3)
3. Load MicroLoRA weights from local cache or `GET /v1/lora/latest`
4. Run locally: `feature_extract()` → LoRA transform → L2 normalize
5. Search: send embedding to brain, or search local cache
Offline: steps 1-4 use cached artifacts. Step 5 searches local cache only.
## 8. Runtime Selection
| Environment | Runtime | Resource Control | Isolation |
|-------------|---------|-----------------|-----------|
| Browser | `WebAssembly.instantiate` in Worker | 25ms wall clock + 8KB input cap | Worker sandbox, no imports |
| Server | wasmtime 18+ | Fuel (~25ms ref CPU) + 16 page memory | No imports, epoch interruption |
| Appliance | wasmtime 18+ | Fuel (~25ms ref CPU) + 16 page memory | No imports, fuel metering |
| Test | wasmtime | Fuel + conformance vector check | No imports |
All environments produce bit-identical results for the same input and node SHA, verified by output byte hash comparison (Section 5.2).
## 9. Security Summary
### 9.1 Module Validation at Publish Time
1. WASM binary is parsed and validated (valid WASM module)
2. Import section must be empty (no imports of any kind)
3. Memory must declare a maximum <= 16 pages
4. No SIMD instructions (module inspection)
5. No f64 usage (module inspection)
6. Required exports present: `memory`, `malloc`, `feature_extract_dim`, `feature_extract`
7. Ed25519 signature verified over canonical binary manifest
8. SHA-256 computed and stored
9. All conformance test vectors pass on server-side wasmtime
10. Contributor reputation >= 0.5
11. Size <= 1MB
### 9.2 Client-Side Verification
1. SHA-256 hash match (WASM bytes vs metadata)
2. Ed25519 signature verification (canonical manifest)
3. Conformance test vectors pass locally
4. Only then: instantiate module
## 10. Implementation Status
### Shipped (in this branch)
| Component | Status |
|-----------|--------|
| `WasmNode` types in server | Shipped |
| `GET /v1/nodes` list endpoint | Shipped |
| `GET /v1/nodes/{id}` metadata endpoint | Shipped |
| `GET /v1/nodes/{id}.wasm` binary endpoint | Shipped |
| `POST /v1/nodes` publish endpoint | Shipped |
| Node storage in `FirestoreClient` | Shipped |
### Deferred (Future Work)
| Component | Priority | Dependency |
|-----------|----------|------------|
| Versioned URL scheme (`/v1/nodes/{id}/{version}/...`) | High | Route refactor |
| `DELETE /v1/nodes/{id}/{version}` revocation | High | Audit logging |
| Module inspection (SIMD/f64 detection) | High | WASM parser |
| Canonical binary manifest signing | High | Manifest encoder |
| Conformance test vector validation | High | Reference node |
| Content-addressed GCS storage | Medium | GCS integration |
| `sona_feature` v1 reference node | High | WASM compilation |
| Browser demo HTML | Medium | Reference node |
| Worker isolation wrapper | Medium | Browser demo |
| wasmtime server-side runner | Medium | Server integration |
| MCP tools (`brain_node_list`, `brain_node_get`) | Low | All endpoints |
## 11. Answers to Design Questions
**Q: Do you want these WASM nodes to run in browser only, or also on the appliance and server with the same bytecode?**
A: All three. Browser, appliance, and server execute the same WASM bytecode with bit-identical results, verified by output byte hash comparison across environments.
**Q: Should nodes be public read like memory search, or should node download be gated because it encodes proprietary heuristics?**
A: Public read. The proprietary value is in the MicroLoRA weights (learned from quality signals via federation), not in the deterministic feature extraction logic. Gating node download would kill the browser demo path for zero competitive benefit.
**Q: Do you want the v1 node ABI to support only feature_extract, or do you want score and validate in v1 as well?**
A: `feature_extract` only for v1. One function, one ABI, one determinism proof. `score` and `validate` are v2 after the cross-platform guarantee is proven on the simpler interface.
**Q: Should nodes be allowed to include a tiny embedded lookup table, or must they be pure hashing with no internal data beyond constants?**
A: Lookup tables are allowed. Token maps, n-gram tables, and normalization tables compiled into the WASM data section are fine — the data section is part of the signed bytecode and is immutable at runtime. This is necessary for practical feature extraction.
## 12. Acceptance Criteria
### Deterministic Cross-Platform Verification
- Pick 20 canonical inputs (including Unicode edge cases, empty string, max length)
- Store expected output SHA-256 values in node metadata `conformance.test_vectors`
- Browser demo shows green for all 20 vectors
- Server and appliance startup self-tests pass the same 20 vectors
- Compare output **byte hashes** (SHA-256 of raw f32 bytes), not float arrays
- p95 execution under 25ms for 128-dim output on all three environments
### Integration
- Browser produces embedding via WASM `feature_extract` + JS MicroLoRA transform
- Browser embedding and server-produced embedding match **byte for byte** given the same node SHA and the same LoRA epoch
- `brain_search` returns identical top-5 results for browser-produced vs server-produced embeddings
### Security
- Module with non-empty import section: rejected at publish time
- Module without memory maximum: rejected at publish time
- Module with SIMD or f64: rejected at publish time (when module inspection is implemented)
- Module exceeding 1MB: rejected at publish time
- Module without valid Ed25519 signature: rejected by client
- Conformance vector failure: node rejected at runtime
- Revoked node: returns 410 Gone
## 13. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-058 | Hash Security Optimization — the feature extraction algorithm nodes implement |
| ADR-059 | Shared Brain Google Cloud — infrastructure serving node binaries |
| ADR-060 | Shared Brain Capabilities — MicroLoRA weights that complement WASM features |
| ADR-062 | Brainpedia Architecture — pages can reference WASM nodes for validation |

View file

@ -0,0 +1,210 @@
# ADR-064: Pi Brain Infrastructure & Landing Page
**Status**: Accepted, Deployed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-066 (SSE MCP Transport)
## 1. Context
The Shared Brain (ADR-059, ADR-060) requires a production deployment surface that is both functional (serving REST and MCP endpoints) and discoverable (a public-facing landing page that communicates what the brain is). A bare API with no human-readable entry point creates adoption friction. Developers who visit the domain should immediately understand the system and how to connect.
Google Cloud Run provides a serverless container platform with automatic TLS, scaling to zero, and custom domain mapping. Custom domains (pi.ruv.io, the Unicode variant) make the brain addressable under a memorable, branded namespace. A landing page with a Three.js visualization gives the project a distinctive identity rooted in the Foundation (Asimov) sci-fi aesthetic.
## 2. Decision
Deploy `mcp-brain-server` as a Cloud Run service (`ruvbrain`) in `us-central1`. Map custom domains through Google Cloud's domain verification and Cloudflare DNS. Embed static HTML landing pages at compile time using `include_str!` so the binary is fully self-contained with no runtime file I/O for static content.
## 3. Architecture
### 3.1 Cloud Run Service
| Property | Value |
|----------|-------|
| Service name | `ruvbrain` |
| Region | `us-central1` |
| Image | Multi-stage Dockerfile: `rust:1.85-bookworm` builder, `debian:bookworm-slim` runtime |
| Port | 8080 (configured via `PORT` env var) |
| Scaling | 0-10 instances, 2 CPU / 2Gi RAM per instance |
| Concurrency | Default (80 requests per instance) |
| Startup | `mcp-brain-server` binary, tracing to stderr |
The Dockerfile uses a two-stage build. The builder stage compiles the full workspace with `cargo build --release -p mcp-brain-server`. The runtime stage copies only the binary and installs `ca-certificates` for HTTPS outbound calls to Firestore and GCS.
### 3.2 Custom Domains
| Domain | Type | Target |
|--------|------|--------|
| `pi.ruv.io` | CNAME | `ghs.googlehosted.com` |
| `xn--1xa.ruv.io` (pi.ruv.io) | CNAME | `ghs.googlehosted.com` |
DNS is managed through Cloudflare. The CNAME records point to Google's hosted services endpoint, which handles TLS termination and routes to the Cloud Run service. Google Cloud domain mapping verifies ownership and provisions managed TLS certificates automatically.
### 3.3 Persistence Layer
**Firestore**: Stores brain memories, page metadata, WASM node metadata, contributor reputations, and LoRA federation state. The `FirestoreClient` hydrates an in-memory cache on startup via `load_from_firestore()`. Writes are dual-written to both local cache and Firestore. When `FIRESTORE_URL` is not set, the server operates in local-only mode for development.
**Google Cloud Storage (GCS)**: Stores RVF containers and WASM node binaries. Content-addressed storage for WASM nodes uses `gs://ruvector-brain-{region}/nodes/sha256/{hash}.wasm`. The `GcsClient` handles uploads and signed URL generation.
### 3.4 Landing Page
The root route (`/`) serves a Three.js Prime Radiant visualization. The page renders an interactive 3D particle system representing the brain's knowledge topology. The aesthetic follows a Foundation (Asimov) sci-fi theme: dark background, blue-white particle fields, Encyclopedia Galactica typography.
Implementation: `include_str!("../static/index.html")` embeds the HTML at compile time. The handler returns the static content with `Content-Type: text/html; charset=utf-8` and `Cache-Control: public, max-age=300`. No runtime filesystem access required.
### 3.5 Origin Story
The `/origin` route serves an animated narrative page explaining the brain's purpose and design philosophy. Same embed pattern: `include_str!("../static/origin.html")`. Same cache headers.
### 3.6 Static Embed Pattern
```rust
async fn landing_page() -> (StatusCode, [(HeaderName, &'static str); 2], &'static str) {
(
StatusCode::OK,
[
(CONTENT_TYPE, "text/html; charset=utf-8"),
(CACHE_CONTROL, "public, max-age=300"),
],
include_str!("../static/index.html"),
)
}
```
This pattern has three advantages:
1. **Zero runtime I/O**: No file reads, no path resolution, no directory traversal risk
2. **Single binary deployment**: The container image contains one executable with no static file dependencies
3. **Compile-time verification**: If the HTML file is missing, the build fails immediately
## 4. Implementation
### 4.1 Router Structure
The `create_router()` function in `routes.rs` constructs the full axum Router:
```rust
Router::new()
.route("/", get(landing_page))
.route("/origin", get(origin_page))
.route("/v1/health", get(health))
// ... 14 REST endpoints (ADR-060)
// ... 6 Brainpedia endpoints (ADR-062)
// ... 5 WASM node endpoints (ADR-063)
.route("/sse", get(sse_handler))
.route("/messages", post(messages_handler))
.layer(CorsLayer::new()
.allow_origin([
"https://brain.ruv.io",
"https://pi.ruv.io",
"https://ruvbrain-875130704813.us-central1.run.app",
"http://localhost:8080",
"http://127.0.0.1:8080",
]))
.layer(TraceLayer::new_for_http())
.layer(RequestBodyLimitLayer::new(1_048_576)) // 1MB
```
### 4.2 Health Endpoint
`GET /v1/health` reports service identity, version, uptime, and persistence mode:
```json
{
"status": "ok",
"version": "0.1.0",
"domain": "pi.ruv.io",
"uptime_seconds": 3600,
"persistence_mode": "firestore"
}
```
### 4.3 Application State
The `AppState` struct holds all shared service state:
| Field | Type | Purpose |
|-------|------|---------|
| `store` | `Arc<FirestoreClient>` | Memory and page persistence |
| `gcs` | `Arc<GcsClient>` | Binary artifact storage |
| `graph` | `Arc<RwLock<KnowledgeGraph>>` | Mincut topology graph |
| `rate_limiter` | `Arc<RateLimiter>` | BudgetTokenBucket rate limiting |
| `ranking` | `Arc<RwLock<RankingEngine>>` | Search result ranking |
| `cognitive` | `Arc<RwLock<CognitiveEngine>>` | DentateGyrus pattern separation |
| `drift` | `Arc<RwLock<DriftMonitor>>` | Embedding centroid drift tracking |
| `aggregator` | `Arc<ByzantineAggregator>` | LoRA weight aggregation |
| `domain_engine` | `Arc<RwLock<DomainExpansionEngine>>` | Cross-domain transfer |
| `sona` | `Arc<RwLock<SonaEngine>>` | Embedding feature extraction |
| `lora_federation` | `Arc<RwLock<LoraFederationStore>>` | Federated LoRA state |
| `nonce_store` | `Arc<NonceStore>` | Challenge nonce replay protection |
| `sessions` | `Arc<DashMap<String, mpsc::Sender>>` | SSE session management |
### 4.4 Deployment Flow
1. Build container image from workspace root using the `crates/mcp-brain-server/Dockerfile`
2. Push to Google Artifact Registry
3. Deploy to Cloud Run with `gcloud run deploy ruvbrain`
4. Map custom domains via Google Cloud Console
5. Configure Cloudflare CNAME records pointing to `ghs.googlehosted.com`
6. Verify TLS certificate provisioning
### 4.5 Endpoint Summary
The deployed server exposes the following route groups:
| Group | Routes | Source ADR |
|-------|--------|-----------|
| Landing pages | `/`, `/origin` | ADR-064 (this document) |
| Infrastructure | `/v1/health`, `/v1/challenge` | ADR-059 |
| Memories (CRUD) | `/v1/memories`, `/v1/memories/search`, `/v1/memories/list`, `/v1/memories/:id`, `/v1/memories/:id/vote` | ADR-060 |
| Transfer & Monitoring | `/v1/transfer`, `/v1/drift`, `/v1/partition`, `/v1/status` | ADR-060 |
| LoRA Federation | `/v1/lora/latest`, `/v1/lora/submit` | ADR-060 |
| Training | `/v1/training/preferences` | ADR-061 |
| Brainpedia | `/v1/pages`, `/v1/pages/:id`, `/v1/pages/:id/deltas`, `/v1/pages/:id/evidence`, `/v1/pages/:id/promote` | ADR-062 |
| WASM Nodes | `/v1/nodes`, `/v1/nodes/:id`, `/v1/nodes/:id/wasm`, `/v1/nodes/:id/revoke` | ADR-063 |
| MCP SSE | `/sse`, `/messages` | ADR-066 |
Total: 27 routes serving REST API, MCP SSE transport, and static HTML.
### 4.6 Security Layers
The server applies the following security measures at the infrastructure level:
1. **TLS termination**: Handled by Google Cloud Run / Cloudflare edge. The server itself listens on plain HTTP on port 8080.
2. **CORS**: Explicit origin allowlist (5 origins). No wildcards.
3. **Request body limit**: 1MB via tower-http `RequestBodyLimitLayer`. Prevents memory exhaustion from oversized payloads.
4. **Rate limiting**: `BudgetTokenBucket` per contributor. 100 writes/hour, 1000 reads/hour. Stale buckets evicted every 1000 operations.
5. **Challenge nonces**: Single-use, 5-minute TTL. Required for write operations.
6. **PII stripping**: 12-pattern detection on all incoming text fields (filesystem paths, API keys, tokens, email patterns).
7. **Embedding verification**: NaN, Inf, and magnitude checks on all incoming vectors.
8. **Ed25519 signature verification**: For RVF container and WASM node integrity.
9. **SHAKE-256 witness chains**: For provenance verification on all operations.
### 4.7 Tracing and Observability
The server uses `tracing_subscriber` with `EnvFilter` for log level control. Logs are written to stderr (Cloud Run captures stderr as structured logs). The `TraceLayer` middleware logs HTTP request/response metadata including method, path, status code, and latency.
The health endpoint provides basic liveness/readiness signaling. Cloud Run uses this for automatic health checks and instance lifecycle management.
## 5. Consequences
### Positive
- **Single binary deployment**: No static file serving complexity, no CDN configuration needed for the landing page
- **Zero-cost at rest**: Cloud Run scales to zero when idle, no persistent infrastructure cost
- **Branded access**: `pi.ruv.io` is memorable and communicates the mathematical/scientific identity
- **Self-documenting**: Visiting the domain explains the system without needing external documentation
- **Unified surface**: REST API, MCP SSE, and landing pages all served from the same binary on the same port
### Negative
- **HTML changes require recompilation**: Modifying the landing page or origin story requires a full cargo build and redeploy. This is acceptable because these pages change infrequently.
- **Single region**: `us-central1` only. Multi-region would require Cloud Run service mesh or global load balancer. Deferred until latency becomes a concern.
- **Cloud vendor lock-in**: Firestore and GCS are Google-specific. The `FirestoreClient` and `GcsClient` abstractions limit blast radius, but migration would require new storage implementations.
### Neutral
- CORS allows five specific origins. New frontends require a code change and redeploy.
- Request body limit is 1MB globally. WASM node uploads are constrained to this limit, matching the ADR-063 spec.
- The `read_only` flag on `AppState` enables emergency read-only mode without redeployment, controlled via an atomic boolean.

View file

@ -0,0 +1,209 @@
# ADR-065: npm Publishing Strategy
**Status**: Accepted
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-064 (Pi Brain Infrastructure), ADR-066 (SSE MCP Transport), ADR-063 (WASM Executable Nodes)
## 1. Context
The RuVector project produces 48+ npm packages under the `@ruvector/` scope. These range from core TypeScript libraries and WASM bindings to MCP server wrappers and CLI tools. A new package, `@ruvector/pi-brain`, bundles CLI, SDK, and MCP stdio access to the Shared Brain into a single installable unit.
Without a defined publishing strategy, interdependent packages can break consumers if published in the wrong order, pre-release versions can leak into production, and TypeScript compilation errors can ship as broken packages. This ADR documents the publishing order, semver strategy, and authentication setup that govern all npm releases.
## 2. Decision
Adopt a structured publishing pipeline: categorize packages, enforce dependency-ordered publishing, use semver with pre-release tags for unstable packages, and require TypeScript compilation to succeed before any publish. All packages are published under the `ruvnet` npm account.
## 3. Architecture
### 3.1 Package Categories
| Category | Description | Examples |
|----------|-------------|---------|
| **Core** | Foundational libraries with no RuVector dependencies | `@ruvector/types`, `@ruvector/utils` |
| **WASM** | WebAssembly bindings compiled from Rust crates | `@ruvector/solver-wasm`, `@ruvector/rvf-wasm` |
| **MCP** | Model Context Protocol server packages | `@ruvector/mcp-brain`, `@ruvector/mcp-gate` |
| **CLI** | Command-line tools | `@ruvector/pi-brain`, `@ruvector/cli` |
| **Infrastructure** | Build tools, codegen, testing utilities | `@ruvector/build-tools`, `@ruvector/test-utils` |
### 3.2 The `@ruvector/pi-brain` Package
`@ruvector/pi-brain` is the primary user-facing npm package for the Shared Brain. It provides three interfaces in one package:
**CLI**: `npx @ruvector/pi-brain search "tokio deadlock"` — search the brain from the command line. `npx @ruvector/pi-brain share --category debug --title "..." --content "..."` — share knowledge.
**SDK**: Programmatic TypeScript API for integrating brain capabilities into applications. Handles authentication, embedding, and the full REST API surface.
**MCP stdio**: `@ruvector/pi-brain mcp` — starts a JSON-RPC stdio server implementing the MCP protocol. Claude Code connects via `claude mcp add pi-brain -- npx @ruvector/pi-brain mcp`.
### 3.3 Publish Order
Packages must be published in dependency order. A package can only be published after all of its `@ruvector/` dependencies have been published at the required version.
**Tier 1 — No `@ruvector/` dependencies**:
- `@ruvector/types`
- `@ruvector/utils`
- `@ruvector/build-tools`
**Tier 2 — Depends on Tier 1 only**:
- `@ruvector/solver-wasm` (depends on `@ruvector/types`)
- `@ruvector/rvf-wasm` (depends on `@ruvector/types`)
- `@ruvector/test-utils` (depends on `@ruvector/types`, `@ruvector/utils`)
**Tier 3 — Depends on Tier 1 and/or Tier 2**:
- `@ruvector/mcp-brain` (depends on `@ruvector/types`)
- `@ruvector/mcp-gate` (depends on `@ruvector/types`)
- `@ruvector/pi-brain` (depends on `@ruvector/types`, `@ruvector/mcp-brain`)
**Tier 4 — Meta-packages and aggregators**:
- `@ruvector/cli` (depends on multiple Tier 2-3 packages)
Within a tier, packages can be published in any order.
### 3.4 Semver Strategy
| Version Range | Meaning | Tag |
|---------------|---------|-----|
| `0.x.y` | Pre-1.0, breaking changes expected between minors | `latest` |
| `x.y.z-alpha.N` | Active development, not for production | `alpha` |
| `x.y.z-beta.N` | Feature-complete, testing in progress | `beta` |
| `x.y.z-rc.N` | Release candidate, final validation | `rc` |
| `x.y.z` | Stable release | `latest` |
Pre-release versions are published with explicit dist-tags:
```bash
npm publish --tag alpha
npm publish --tag beta
```
The `latest` tag is only set on stable releases. This prevents `npm install @ruvector/pi-brain` from pulling pre-release versions.
### 3.5 TypeScript Compilation Requirements
Every package with TypeScript source must pass these checks before publish:
1. `tsc --noEmit` succeeds with zero errors
2. `tsc --declaration` generates `.d.ts` files
3. `package.json` includes `types` or `typings` field pointing to the declaration entry point
4. `exports` map includes `types` condition for each entry point
Packages that fail TypeScript compilation are blocked from publishing. This is enforced by running `npm run build` (which includes `tsc`) as the first step of the publish pipeline.
## 4. Implementation
### 4.1 Authentication
npm authentication uses the `ruvnet` account. Credentials are stored in the project `.env` file and loaded into `~/.npmrc` before publishing. Verify with `npm whoami`.
The npm token is scoped to the `@ruvector/` scope with publish permissions. Read-only tokens are used in CI for install-only workflows.
### 4.2 Pre-Publish Checklist
For each package:
1. Verify `npm whoami` returns `ruvnet`
2. Run `npm run build` (TypeScript compilation + any bundling)
3. Run `npm test` (all tests must pass)
4. Verify `package.json` version matches the intended release
5. Check that `files` field in `package.json` includes only intended artifacts
6. Run `npm pack --dry-run` to inspect the tarball contents
7. Publish: `npm publish --access public`
### 4.3 WASM Package Build
WASM packages require a Rust compilation step before the npm publish:
1. `cargo build --release --target wasm32-unknown-unknown -p <crate>`
2. `wasm-bindgen` or `wasm-pack` generates the JS/TS bindings
3. Copy generated files into the npm package directory
4. Run TypeScript compilation on the wrapper code
5. Publish as a standard npm package
### 4.4 Solver Crate Publish Order (Cargo)
For the Rust solver crates published to crates.io (not npm), the order is:
1. `ruvector-solver` first (no dependencies)
2. `ruvector-solver-wasm` second (depends on `ruvector-solver`)
3. `ruvector-solver-node` third (depends on `ruvector-solver`)
Always run `cargo publish --dry-run --allow-dirty` before real publish. `ruvector-profiler` has `publish = false` and is intentionally not publishable.
### 4.5 Version Coordination
When a breaking change occurs in a Tier 1 package, all dependent packages must be updated and republished. The procedure is:
1. Publish the updated Tier 1 package with the new major/minor version
2. Update `package.json` in all dependent packages to reference the new version
3. Run `npm install` in each dependent package to verify resolution
4. Run tests in each dependent package
5. Publish dependent packages in tier order
For non-breaking changes (patch versions), only the changed package needs republishing. Dependents using caret ranges (`^x.y.z`) automatically resolve the new patch.
### 4.6 Package.json Standards
All `@ruvector/` packages must include:
```json
{
"name": "@ruvector/<package>",
"version": "x.y.z",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector"
},
"engines": { "node": ">=18" },
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": ["dist/", "README.md", "LICENSE"]
}
```
The `files` field is explicitly set to prevent accidental inclusion of source maps, test fixtures, `.env` files, or other development artifacts in the published tarball.
### 4.7 CI Integration
The publish pipeline runs in GitHub Actions:
1. **Trigger**: Manual workflow dispatch with package name and version as inputs
2. **Auth**: npm token from GitHub Secrets, loaded into `~/.npmrc`
3. **Build**: `npm run build` in the package directory
4. **Test**: `npm test` in the package directory
5. **Dry run**: `npm pack --dry-run` to verify tarball contents
6. **Publish**: `npm publish --access public` (or `--tag alpha/beta` for pre-releases)
7. **Verify**: `npm view @ruvector/<package> version` confirms the published version
The workflow rejects publishes if the version already exists on the registry (npm returns 403 for duplicate versions).
## 5. Consequences
### Positive
- **No broken installs**: Dependency-ordered publishing ensures consumers never pull a package whose dependencies are not yet available
- **Safe defaults**: Pre-release tags prevent accidental production use of unstable versions
- **Type safety**: Mandatory TypeScript compilation catches type errors before they reach consumers
- **Single account**: All packages under `ruvnet` with `@ruvector/` scope provides consistent ownership and discoverability
- **Explicit file lists**: The `files` field prevents credential leaks and keeps tarballs small
### Negative
- **Manual coordination**: Publishing 48+ packages in order requires discipline. Automation (a publish script that resolves the dependency graph and publishes in topological order) is deferred but recommended.
- **WASM build complexity**: WASM packages require both Rust and Node.js toolchains. Build failures in either chain block the publish.
- **ESM-only**: All packages use `"type": "module"`. CommonJS consumers must use dynamic `import()` or a bundler. This is a deliberate choice — the ecosystem is moving to ESM and dual-packaging adds complexity.
### Neutral
- The `@ruvector/pi-brain` package combining CLI + SDK + MCP in one package increases the install size but simplifies the getting-started experience. Users who only need the SDK can tree-shake unused CLI code.
- Node.js >= 18 is required. This matches the current LTS baseline and enables native `fetch`, `structuredClone`, and other modern APIs.

View file

@ -0,0 +1,223 @@
# ADR-066: SSE MCP Transport
**Status**: Accepted, Deployed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-062 (Brainpedia), ADR-063 (WASM Executable Nodes), ADR-064 (Pi Brain Infrastructure)
## 1. Context
The Shared Brain's primary MCP interface is stdio-based: the `mcp-brain` crate runs as a local process within a Claude Code session, communicating via JSON-RPC over stdin/stdout. This works well for local sessions but cannot serve remote clients — browsers, CI pipelines, or Claude Code sessions that want to connect without installing a local binary.
MCP over Server-Sent Events (SSE) solves this. The client opens a long-lived `GET /sse` connection to receive server-pushed events, and sends tool calls via `POST /messages?sessionId=<uuid>`. This is a standard MCP transport that Claude Code supports natively via `claude mcp add <name> --url <sse-url>`.
The SSE transport is hosted on the same `mcp-brain-server` binary that serves the REST API (ADR-064), at `pi.ruv.io/sse`. No additional infrastructure is required.
## 2. Decision
Implement MCP SSE transport as two routes (`/sse` and `/messages`) on the existing `mcp-brain-server` axum router. Tool calls received via SSE are proxied to the REST API endpoints via HTTP loopback, reusing all existing authentication, rate limiting, and verification logic. Session state is managed via `DashMap<String, mpsc::Sender<String>>`.
## 3. Architecture
### 3.1 Protocol Flow
```
Client Server (pi.ruv.io)
| |
| GET /sse |
|-------------------------------------->|
| event: endpoint |
| data: /messages?sessionId=<uuid> |
|<--------------------------------------|
| |
| POST /messages?sessionId=<uuid> |
| {"jsonrpc":"2.0","method":"initialize",...}
|-------------------------------------->|
| event: message |
| data: {"jsonrpc":"2.0","result":{...}}
|<--------------------------------------|
| |
| POST /messages?sessionId=<uuid> |
| {"jsonrpc":"2.0","method":"tools/call","params":{"name":"brain_search",...}}
|-------------------------------------->|
| event: message |
| data: {"jsonrpc":"2.0","result":{...}}
|<--------------------------------------|
| |
| (keepalive comments) |
|<--------------------------------------|
```
1. Client opens `GET /sse`. Server generates a UUID session ID, creates an `mpsc::channel`, stores the sender in the session map, and returns an SSE stream.
2. The first SSE event (`endpoint`) tells the client where to POST messages.
3. Client sends JSON-RPC requests to `POST /messages?sessionId=<uuid>`.
4. Server parses the request, dispatches to the appropriate handler, and sends the response back through the SSE channel.
5. KeepAlive comments prevent connection timeouts on proxies and load balancers.
6. On disconnect, the session is cleaned up from the `DashMap`.
### 3.2 Session Management
```rust
// In AppState
sessions: Arc<DashMap<String, tokio::sync::mpsc::Sender<String>>>
```
Each SSE connection creates a session with:
- A UUID session ID
- An `mpsc::channel(64)` for buffering responses
- A `DashMap` entry mapping session ID to the channel sender
The SSE stream reads from the channel receiver. When the client disconnects (stream drops), the cleanup closure removes the session from the map. The channel buffer of 64 prevents slow clients from blocking the server while providing backpressure.
### 3.3 MCP Protocol Implementation
The `/messages` handler implements the MCP protocol methods:
| Method | Handler | Description |
|--------|---------|-------------|
| `initialize` | Inline | Returns protocol version `2024-11-05`, server name `pi-brain`, capabilities |
| `initialized` | Inline | Acknowledgment, returns empty result |
| `notifications/initialized` | Inline | Notification acknowledgment |
| `tools/list` | `mcp_tool_definitions()` | Returns the full tool catalog |
| `tools/call` | `handle_mcp_tool_call()` | Dispatches to HTTP loopback proxy |
### 3.4 HTTP Loopback Proxy
Tool calls are not handled directly in the SSE message handler. Instead, they are proxied to the server's own REST API via HTTP loopback. This ensures that:
1. All existing middleware (CORS, rate limiting, body size limits, tracing) applies uniformly
2. Authentication and verification logic is not duplicated
3. The REST API and MCP SSE surface expose identical behavior
4. Testing is simplified — REST endpoint tests cover both transport paths
The proxy constructs an HTTP request to `http://127.0.0.1:{PORT}/v1/...` with the appropriate method, path, and body derived from the MCP tool call arguments.
### 3.5 Tool Catalog
22 tools are exposed via the SSE transport, grouped into four categories:
**Core Brain (10)**: `brain_share`, `brain_search`, `brain_get`, `brain_vote`, `brain_transfer`, `brain_drift`, `brain_partition`, `brain_list`, `brain_delete`, `brain_status`
**LoRA Sync (1)**: `brain_sync`
**Brainpedia (6, ADR-062)**: `brain_page_create`, `brain_page_get`, `brain_page_delta`, `brain_page_deltas`, `brain_page_evidence`, `brain_page_promote`
**WASM Nodes (5, ADR-063)**: `brain_node_list`, `brain_node_publish`, `brain_node_get`, `brain_node_wasm`, `brain_node_revoke`
Each tool definition includes a JSON Schema `inputSchema` specifying required and optional parameters, types, and descriptions.
## 4. Implementation
### 4.1 SSE Handler
```rust
async fn sse_handler(
State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let session_id = Uuid::new_v4().to_string();
let (tx, rx) = tokio::sync::mpsc::channel::<String>(64);
state.sessions.insert(session_id.clone(), tx);
let stream = async_stream::stream! {
// First event: tell client where to POST
yield Ok(Event::default()
.event("endpoint")
.data(format!("/messages?sessionId={session_id}")));
// Stream responses from the channel
let mut rx = rx;
while let Some(msg) = rx.recv().await {
yield Ok(Event::default().event("message").data(msg));
}
// Cleanup on disconnect
sessions_cleanup.remove(&session_id_cleanup);
};
Sse::new(stream).keep_alive(KeepAlive::default())
}
```
### 4.2 Message Handler
```rust
async fn messages_handler(
State(state): State<AppState>,
Query(query): Query<McpMessageQuery>,
body: String,
) -> StatusCode {
let sender = match state.sessions.get(&query.session_id) {
Some(s) => s.clone(),
None => return StatusCode::NOT_FOUND,
};
let request: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
// Send JSON-RPC parse error through the SSE channel
let _ = sender.send(error_response(-32700, e)).await;
return StatusCode::ACCEPTED;
}
};
let response = match method {
"initialize" => { /* protocol handshake */ },
"tools/list" => { /* return tool catalog */ },
"tools/call" => { /* HTTP loopback proxy */ },
_ => { /* method not found */ },
};
let _ = sender.send(serde_json::to_string(&response).unwrap()).await;
StatusCode::ACCEPTED
}
```
### 4.3 Connection
Claude Code clients connect with:
```bash
claude mcp add pi --url https://pi.ruv.io/sse
```
This registers the SSE endpoint. Claude Code opens the SSE connection, reads the `endpoint` event, and sends all subsequent MCP requests to the `/messages` URL with the provided session ID.
### 4.4 CORS Configuration
The SSE endpoint shares the same CORS layer as the REST API:
| Allowed Origin | Purpose |
|----------------|---------|
| `https://brain.ruv.io` | Legacy brain domain |
| `https://pi.ruv.io` | Primary domain |
| `https://ruvbrain-875130704813.us-central1.run.app` | Direct Cloud Run URL |
| `http://localhost:8080` | Local development |
| `http://127.0.0.1:8080` | Local development (IP) |
Allowed methods: GET, POST, DELETE, OPTIONS. Allowed headers: Authorization, Content-Type, Accept.
### 4.5 KeepAlive
`Sse::new(stream).keep_alive(KeepAlive::default())` sends periodic SSE comments (`:keepalive` lines) to prevent intermediate proxies, load balancers, and Cloud Run from closing idle connections. The default interval is 15 seconds.
## 5. Consequences
### Positive
- **Zero additional infrastructure**: SSE runs on the same binary and port as the REST API
- **Native Claude Code support**: `claude mcp add --url` is the standard way to connect remote MCP servers
- **Full tool parity**: All 22 tools available via both stdio (local `mcp-brain`) and SSE (remote `pi.ruv.io`)
- **Reused middleware**: Rate limiting, CORS, authentication, and verification apply uniformly via the loopback proxy
### Negative
- **Single-direction streaming**: SSE is server-to-client only. The client must POST to a separate endpoint. WebSocket would allow bidirectional messaging but is not part of the MCP spec.
- **Session memory**: Each active SSE connection holds a channel and a `DashMap` entry. Under high concurrent connections this grows linearly. The 64-message buffer per session bounds memory per connection.
- **Cloud Run timeout**: Cloud Run has a maximum request timeout (default 5 minutes, configurable up to 60 minutes). Long-lived SSE connections that exceed this timeout are terminated. KeepAlive prevents idle disconnects, but the maximum lifetime is bounded by Cloud Run's configuration.
### Neutral
- The loopback proxy adds one local HTTP hop per tool call. On the same machine this adds sub-millisecond latency, which is negligible compared to Firestore round-trips and embedding computation.
- Error responses from the REST API are translated into JSON-RPC error format before being sent through the SSE channel.

View file

@ -0,0 +1,222 @@
# ADR-067: MCP Gate Permit System
**Status**: Accepted, Implemented
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-014 (Coherence Engine), ADR-058 (Hash Security Optimization), ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities)
## 1. Context
AI agents executing in production environments need a decision gate between intent and action. An agent that can call any tool without verification is a liability. The coherence gate (ADR-014) provides the mathematical foundation — anytime-valid e-processes, conformal prediction sets, mincut structural witnesses — but it needs an MCP-native interface so Claude Code agents can request permissions via the standard tool-calling protocol.
The `mcp-gate` crate (`crates/mcp-gate/`) wraps the `cognitum-gate-tilezero` coherence gate in an MCP stdio server. It exposes three tools: `permit_action` (request permission), `get_receipt` (audit trail), and `replay_decision` (deterministic verification). All decisions are recorded in a cryptographic witness chain. Contributor authentication uses SHAKE-256 pseudonym derivation, and access is rate-limited via BudgetTokenBucket.
## 2. Decision
Implement `mcp-gate` as a standalone MCP stdio server that wraps the existing `cognitum-gate-tilezero` gate. Use SHAKE-256 for pseudonym derivation (consistent with the brain server's authentication scheme), challenge nonces for replay protection, and BudgetTokenBucket for rate limiting. All gate decisions produce cryptographically chained witness receipts.
## 3. Architecture
### 3.1 Crate Structure
```
crates/mcp-gate/
src/
lib.rs -- Public API, re-exports
main.rs -- Entry point (McpGateServer::run_stdio)
server.rs -- MCP protocol handler (JSON-RPC over stdio)
tools.rs -- Tool implementations (permit, receipt, replay)
types.rs -- Request/response types, JSON-RPC types
Cargo.toml
```
The crate depends on:
- `cognitum-gate-tilezero`: The coherence gate engine (TileZero)
- `tokio`: Async runtime for stdio processing
- `serde` / `serde_json`: JSON-RPC serialization
- `thiserror`: Error types
### 3.2 MCP Tools
**`permit_action`**: Request permission for an action. The gate evaluates three witnesses:
| Witness | What it measures | Output |
|---------|-----------------|--------|
| Structural | Mincut graph analysis of the action's connectivity | `cut_value`, `partition` status, critical edges |
| Predictive | Conformal prediction set for the action's outcome | `set_size`, `coverage` target |
| Evidential | Anytime-valid e-process accumulation | `e_value`, `verdict` (accept/continue/reject) |
Returns one of three decisions:
- **Permit**: Action allowed. Returns a `PermitToken` (base64-encoded, time-bounded) and a witness receipt.
- **Defer**: Action escalated. Returns escalation info (reason, suggested reviewer) and a witness receipt.
- **Deny**: Action blocked. Returns denial reason and a witness receipt.
**`get_receipt`**: Retrieve a witness receipt by sequence number. Each gate decision produces a receipt containing the decision, timestamp, witness summary, and a hash linking to the previous receipt. The chain is cryptographic — tampering with any receipt breaks the chain.
**`replay_decision`**: Deterministically replay a past decision given the same inputs and state snapshot. Optionally verifies the hash chain integrity up to the replayed sequence number. Returns whether the replayed decision matches the original.
### 3.3 SHAKE-256 Pseudonym Derivation
Contributor pseudonyms are derived from API keys using SHAKE-256, identical to the brain server (ADR-059, ADR-060):
```rust
pub fn from_api_key(api_key: &str) -> AuthenticatedContributor {
let mut hasher = Shake256::default();
hasher.update(b"ruvector-brain-pseudonym:");
hasher.update(api_key.as_bytes());
let mut reader = hasher.finalize_xof();
let mut buf = [0u8; 16];
reader.read(&mut buf);
let pseudonym = hex::encode(buf);
// ...
}
```
This produces a 32-character hex pseudonym (128-bit) that:
- Is deterministic: same API key always produces the same pseudonym
- Is irreversible: the API key cannot be recovered from the pseudonym
- Uses a domain-separated prefix (`ruvector-brain-pseudonym:`) to prevent cross-system correlation
### 3.4 Challenge Nonce Replay Protection
Write operations (including `permit_action`) require a challenge nonce to prevent replay attacks:
1. Client calls `GET /v1/challenge` to receive a fresh nonce
2. Server generates a random nonce, stores it with a 5-minute TTL
3. Client includes the nonce in the write request
4. Server verifies the nonce exists and has not expired
5. Server consumes the nonce (single-use)
Nonces are stored in a `NonceStore` backed by a `DashMap` with periodic TTL eviction.
### 3.5 Rate Limiting
The `BudgetTokenBucket` pattern (shared with the brain server) provides per-contributor rate limiting:
```rust
pub struct RateLimiter {
write_buckets: DashMap<String, TokenBucket>,
read_buckets: DashMap<String, TokenBucket>,
write_limit: u32, // 100 writes/hour
read_limit: u32, // 1000 reads/hour
window: Duration, // 1 hour
}
```
Each contributor (identified by pseudonym) gets independent token buckets for reads and writes. Buckets refill at window boundaries. Stale buckets (unused for 2 windows) are periodically evicted to prevent unbounded memory growth. The cleanup runs every 1000 operations.
### 3.6 Multi-Factor Reputation Scoring
The reputation system gates access levels:
```
composite = accuracy^2 * uptime * stake_weight
```
| Factor | Measurement | Update |
|--------|-------------|--------|
| `accuracy` | Bayesian Beta(1,1) prior, updated from vote outcomes | `(upvotes+1)/(upvotes+downvotes+2)` after min 5 observations |
| `uptime` | EMA of activity frequency | `uptime = 0.95 * uptime + 0.05` per contribution |
| `stake_weight` | Fixed at 1.0 for v1 | Future: based on contribution volume |
- New contributors start at composite ~0.1. Their gate requests are weighted 10x less.
- Contributors with composite < 0.1 are flagged as potential poisoners and their permit requests are auto-denied.
- Inactivity decay: `accuracy *= 0.95^months_inactive`, `uptime *= 0.90^months_inactive`.
### 3.7 Contributor Pseudonym Revocation
A contributor can be revoked by setting their reputation below the poisoning threshold (0.1). The `check_poisoning_penalty` function triggers when:
- The contributor has >= 5 downvotes on their contributions
- Their quality score falls below 0.2
Once penalized, the contributor's pseudonym is effectively revoked — all gate requests return Deny. The pseudonym remains in the system for audit purposes. The contributor can only recover by generating a new API key (and thus a new pseudonym), starting at the cold-start reputation of 0.1.
## 4. Implementation
### 4.1 Server Entry Point
```rust
let server = McpGateServer::new();
server.run_stdio().await.expect("Server failed");
```
The server reads JSON-RPC requests from stdin, dispatches to tool handlers, and writes responses to stdout. The protocol version is `2024-11-05`.
### 4.2 Request/Response Flow
```
stdin -> parse JSON-RPC -> match method:
"initialize" -> return server info + capabilities
"tools/list" -> return [permit_action, get_receipt, replay_decision]
"tools/call" -> match tool name:
"permit_action" -> TileZero::decide() -> PermitResponse | DeferResponse | DenyResponse
"get_receipt" -> TileZero::get_receipt(seq) -> GetReceiptResponse
"replay_decision" -> TileZero::replay(seq) -> ReplayDecisionResponse
-> serialize JSON-RPC -> stdout
```
### 4.3 Permit Action Request
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "permit_action",
"arguments": {
"action_id": "cfg-push-7a3f",
"action_type": "config_change",
"target": { "device": "router-west-03", "path": "/network/interfaces/eth0" },
"context": { "agent_id": "ops-agent-12", "session_id": "sess-abc123", "urgency": "normal" }
}
}
}
```
### 4.4 Permit Response (Permitted)
```json
{
"decision": "permit",
"token": "eyJ0eXAi...",
"valid_until_ns": 1737158400000000000,
"witness": {
"structural": { "cut_value": 12.7, "partition": "stable", "critical_edges": 0 },
"predictive": { "set_size": 3, "coverage": 0.92 },
"evidential": { "e_value": 847.3, "verdict": "accept" }
},
"receipt_sequence": 1847392
}
```
### 4.5 Error Codes
| Code | Meaning |
|------|---------|
| -32001 | Receipt not found |
| -32002 | Chain verification failed |
| -32602 | Invalid request parameters |
| -32603 | Internal server error |
| -32700 | JSON parse error |
## 5. Consequences
### Positive
- **Standard MCP interface**: Any MCP-compatible client (Claude Code, custom agents) can request gate permits without custom integration
- **Cryptographic audit trail**: Every decision is chained. Tampering is detectable. Replays are deterministic.
- **Shared auth scheme**: SHAKE-256 pseudonym derivation is identical between `mcp-gate` and `mcp-brain-server`, enabling cross-system contributor identity without sharing API keys
- **Defense in depth**: Rate limiting + nonce replay protection + reputation gating + cryptographic witnesses form layered protection
### Negative
- **Stdio only**: The MCP gate is stdio-based, not SSE. It must run as a local process. Remote access requires wrapping in a network transport (deferred).
- **Stateful**: The TileZero gate accumulates witness receipts in memory. Long-running sessions with many decisions grow linearly. Persistence to disk is deferred.
### Neutral
- The three-witness model (structural, predictive, evidential) is inherited from `cognitum-gate-tilezero`. The MCP gate does not add new decision logic — it provides protocol access to existing capabilities.
- Pseudonym revocation is soft (reputation penalty) not hard (key revocation). A determined attacker can generate new API keys. This is acceptable because cold-start reputation (0.1) limits the blast radius of new pseudonyms.

View file

@ -0,0 +1,239 @@
# ADR-068: Domain Expansion Transfer Learning
**Status**: Accepted, Implemented
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-057 (Federated RVF Transfer Learning), ADR-060 (Shared Brain Capabilities), ADR-061 (Reasoning Kernel Architecture)
## 1. Context
The Shared Brain (ADR-060) enables cumulative learning within a single domain. But real intelligence growth appears when knowledge from one domain accelerates learning in a different domain. A debugging heuristic from Rust should inform debugging in TypeScript. A planning strategy from infrastructure deployment should transfer to release management.
The `ruvector-domain-expansion` crate (`crates/ruvector-domain-expansion/`) implements cross-domain transfer learning using Meta Thompson Sampling with compact prior transfer. The core insight: true generalization is measured by whether Domain 2 converges faster than Domain 1 did, given priors extracted from Domain 1. If cost curves compress with each new domain, general problem-solving capability is increasing.
## 2. Decision
Implement a two-layer architecture: a policy learning layer (Meta Thompson Sampling with Beta priors) that chooses strategies across context buckets, and an operator layer (deterministic domain kernels) that generates tasks, evaluates solutions, and produces embeddings. Transfer happens through compact priors — not raw trajectories. Verification requires dual conditions: improved target AND not regressed source.
## 3. Architecture
### 3.1 Two-Layer Design
**Policy Learning Layer** (`MetaThompsonEngine`):
- Maintains per-domain, per-bucket, per-arm Beta distribution parameters
- Selects strategy arms via Thompson Sampling (sample from posterior, pick highest)
- Records outcomes as Bayesian posterior updates
- Extracts compact `TransferPrior` summaries for cross-domain shipping
- Seeds new domains with dampened priors from source domains
**Operator Layer** (Domain implementations):
- `RustSynthesisDomain`: Generates Rust function synthesis tasks, evaluates correctness
- `PlanningDomain`: Generates multi-step planning tasks with dependencies and resources
- `ToolOrchestrationDomain`: Generates multi-tool coordination tasks
- Each domain implements `generate_tasks()`, `evaluate()`, `embed()`, `reference_solution()`
### 3.2 Meta Thompson Sampling
Standard Thompson Sampling maintains a Beta(alpha, beta) distribution per arm and samples to select. Meta Thompson Sampling extends this across domains:
1. **Train on Domain 1**: Record outcomes, accumulate per-bucket posteriors
2. **Extract TransferPrior**: Filter to buckets with sufficient evidence (alpha + beta > 12), package as a compact summary
3. **Initialize Domain 2**: Seed with dampened priors from Domain 1 using sqrt-scaling
4. **Train on Domain 2**: The transferred priors give Domain 2 a head start
5. **Measure acceleration**: Compare convergence cycles with vs without transfer
### 3.3 Dampened Sqrt-Scaling Priors
When transferring priors from source to target, raw posteriors would over-commit the target to source-domain strategies. Dampening uses sqrt-scaling to reduce confidence while preserving the mean:
```rust
let dampened = BetaParams {
alpha: 1.0 + (params.alpha - 1.0).sqrt(),
beta: 1.0 + (params.beta - 1.0).sqrt(),
};
```
Example: A source prior of Beta(81, 21) (mean=0.79, very confident) becomes Beta(1 + sqrt(80), 1 + sqrt(20)) = Beta(9.94, 5.47) (mean=0.64, much less confident). The target retains the directional signal (strategy A is probably better than strategy B) but has enough uncertainty to adapt if the source signal does not transfer.
Cost EMA priors are transferred with pessimistic scaling (1.5x) to avoid under-budgeting in an unfamiliar domain.
### 3.4 TransferPrior and PolicyKernel RVF Segments
Transfer artifacts are serialized as RVF segments for the Shared Brain:
| Segment | Type Code | Content |
|---------|-----------|---------|
| `TransferPrior` | `0x30` | Per-bucket, per-arm Beta parameters + cost EMA priors + training cycle count + witness hash |
| `PolicyKernel` | `0x31` | Policy knobs (skip mode, prepass flag, speculation threshold) + holdout scores + fitness |
| `CostCurve` | `0x32` | Ordered data points (cycle, accuracy, cost_per_solve, robustness, violations) + convergence thresholds |
The `rvf_bridge` module (enabled with `feature = "rvf"`) handles serialization via wire-format wrappers that convert `HashMap<ContextBucket, _>` to `Vec<(K, V)>` for JSON-safe encoding. SHAKE-256 witness chains cover the entire serialization for integrity.
### 3.5 CostCurve Acceleration Tracking
The `AccelerationScoreboard` tracks convergence speed across domains:
```rust
pub struct CostCurvePoint {
pub cycle: u64,
pub accuracy: f32,
pub cost_per_solve: f32,
pub robustness: f32,
pub policy_violations: u32,
pub timestamp: f64,
}
```
Convergence thresholds define the acceptance test:
- Target accuracy: 0.95
- Target cost per solve: 0.01
- Target robustness: 0.90
- Max policy violations: 0
A domain has converged when all four thresholds are simultaneously met. The acceleration factor is `baseline_cycles / transfer_cycles` — the ratio of how many cycles the target took without transfer vs with transfer. An acceleration factor > 1.0 means transfer helped.
### 3.6 TransferVerification
The verification protocol enforces the generalization rule:
```rust
impl TransferVerification {
pub fn verify(
source: DomainId,
target: DomainId,
source_before: f32,
source_after: f32, // must not regress beyond 0.01 tolerance
target_before: f32,
target_after: f32, // must improve
baseline_cycles: u64,
transfer_cycles: u64,
) -> Self {
let improved_target = target_after > target_before;
let regressed_source = source_after < source_before - 0.01;
let promotable = improved_target && !regressed_source;
let acceleration_factor = baseline_cycles as f32 / transfer_cycles as f32;
// ...
}
}
```
A transfer delta is promotable only when:
1. `improved_target`: The target domain's score increased after applying the transfer
2. `NOT regressed_source`: The source domain's score did not decrease beyond a 0.01 tolerance
Both conditions must hold. This prevents transfers that help the target at the expense of corrupting the source.
### 3.7 Holdout Evaluation Protocol
Transfers are validated on held-out tasks, not training tasks:
1. `generate_holdouts(tasks_per_domain, difficulty)` creates holdout task sets per domain
2. `evaluate_population()` runs all policy kernels against holdout tasks
3. Holdout scores are recorded per kernel per domain
4. `evolve_population()` selects top performers, mutates, records Pareto front
The holdout set is never used for training. This prevents overfitting to the evaluation metric.
## 4. Implementation
### 4.1 DomainExpansionEngine
The central orchestrator:
```rust
pub struct DomainExpansionEngine {
domains: HashMap<DomainId, Box<dyn Domain>>,
pub thompson: MetaThompsonEngine,
pub population: PopulationSearch,
pub scoreboard: AccelerationScoreboard,
pub meta: MetaLearningEngine,
holdouts: HashMap<DomainId, Vec<Task>>,
counterexamples: HashMap<DomainId, Vec<(Task, Solution, Evaluation)>>,
}
```
Initialized with three domains: `rust_synthesis`, `structured_planning`, `tool_orchestration`. Four strategy arms: `greedy`, `exploratory`, `conservative`, `speculative`. Three difficulty tiers: `easy`, `medium`, `hard`.
### 4.2 Transfer Flow
```rust
// 1. Record outcomes in source domain
engine.evaluate_and_record(&source, task, solution, bucket, arm);
// 2. Initiate transfer (extracts dampened priors)
engine.initiate_transfer(&source, &target);
// 3. Train on target domain (uses transferred priors)
engine.evaluate_and_record(&target, task, solution, bucket, arm);
// 4. Verify the transfer
let verification = engine.verify_transfer(
&source, &target,
source_before, source_after,
target_before, target_after,
baseline_cycles, transfer_cycles,
);
assert!(verification.promotable);
```
### 4.3 Population-Based Policy Search
A population of 8 `PolicyKernel` variants runs in parallel. Each kernel tunes strategy knobs:
- Skip mode (whether to skip low-confidence contexts)
- Prepass flag (whether to run a fast prepass before full evaluation)
- Speculation threshold (when to trigger dual-path execution)
Evolution: evaluate all kernels on holdouts, record in Pareto front (accuracy vs cost vs robustness), keep top performers, mutate, increment generation.
### 4.4 Meta-Learning Engine
Five composable improvements layered on top of Thompson Sampling:
| Component | Purpose |
|-----------|---------|
| `RegretTracker` | Measures cumulative regret vs oracle policy |
| `DecayingBeta` | Time-decays Beta parameters for non-stationary environments |
| `PlateauDetector` | Detects when cost curve flattens, suggests action (increase difficulty, transfer, restart) |
| `ParetoFront` | Maintains non-dominated set of (accuracy, cost, robustness) |
| `CuriosityBonus` | UCB-style exploration bonus for under-visited bucket/arm combinations |
### 4.5 Integration with Shared Brain
The `DomainExpansionEngine` is instantiated in the `mcp-brain-server`'s `AppState`:
```rust
let domain_engine = Arc::new(parking_lot::RwLock::new(
ruvector_domain_expansion::DomainExpansionEngine::new(),
));
```
The `POST /v1/transfer` REST endpoint and the `brain_transfer` MCP tool invoke `engine.initiate_transfer()` and return the `TransferVerification` result.
### 4.6 Counterexample Tracking
Solutions scoring below 0.3 are stored as counterexamples per domain. These serve two purposes:
1. Negative examples for future strategy selection (avoid strategies that produced poor results in similar contexts)
2. Diagnostic data for understanding domain boundaries where transfer fails
## 5. Consequences
### Positive
- **Measurable generalization**: The acceleration scoreboard provides a quantitative answer to "is the system getting smarter across domains?"
- **Safe transfer**: Dampened priors prevent over-commitment to source-domain strategies. Dual verification prevents source regression.
- **Compact transfer artifacts**: TransferPriors are small (per-bucket Beta parameters, not raw trajectories). They serialize to a few KB as RVF segments.
- **Composable meta-learning**: Regret tracking, plateau detection, Pareto optimization, and curiosity bonuses layer independently and can be enabled/disabled per deployment.
### Negative
- **Three domains only**: The current implementation has three hard-coded domains (Rust synthesis, planning, tool orchestration). Adding new domains requires implementing the `Domain` trait and registering with the engine.
- **No online transfer**: Transfer is initiated manually via `initiate_transfer()`. Automatic transfer triggering (e.g., when a domain's cost curve plateaus) is deferred.
- **Population size fixed**: The population search uses 8 kernels. Tuning population size requires code changes.
### Neutral
- The 0.01 tolerance on source regression allows minor noise in evaluation scores without blocking transfers. This is a practical trade-off — evaluation noise from holdout sampling can cause small score fluctuations that are not true regressions.
- Counterexamples grow unboundedly per domain. A pruning strategy (keep top-N by recency or informativeness) is deferred.

View file

@ -0,0 +1,514 @@
# ADR-069: Edge-Net and Pi Brain Integration — Distributed Compute Intelligence
**Status**: Proposed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-062 (Brainpedia Architecture), ADR-063 (WASM Executable Nodes), ADR-064 (Pi Brain Infrastructure), ADR-066 (SSE MCP Transport)
## 1. Context
Two complementary systems exist in the ruvector ecosystem, both deployed on Google Cloud Run:
**Pi Brain** (`ruvbrain` at `pi.ruv.io`) is a centralized shared intelligence substrate. It provides a knowledge graph with Bayesian quality scoring, federated MicroLoRA consensus weights, structured hash embeddings, MinCut graph partitioning, and SONA self-optimization. It exposes 22 tools via SSE MCP transport (`/sse`) and a REST API (`/v1/*`). All operations are protected by witness chains and a challenge-nonce authentication system. It runs as a single Rust binary on Cloud Run.
**Edge-Net** (`examples/edge-net/`) is a distributed P2P browser compute network. Browser visitors contribute idle CPU cycles via Web Workers running Rust/WASM. Contributors earn rUv (Resource Utility Vouchers) based on compute donated. The network provides HNSW vector search, MicroLoRA adaptation, federated gradient gossip, entropy-based consensus, collective memory with hippocampal replay, and an adversarial coherence engine (RAC). It uses Pi-Key Ed25519 identity, a CRDT-based credit ledger, and a browser-based MCP server (`WasmMcpServer`).
These systems operate independently. The brain holds curated knowledge but is bottlenecked by single-origin compute for embedding generation, similarity search, and LoRA training. Edge-net has distributed compute capacity but no centralized knowledge substrate to draw from or contribute to. Connecting them creates a flywheel: edge nodes contribute compute that powers brain operations, brain knowledge improves edge node task routing, and rUv earned from brain operations incentivizes sustained edge participation.
### Current Deployment
| Service | URL | Platform |
|---------|-----|----------|
| `ruvbrain` (pi brain) | `https://pi.ruv.io` | Cloud Run, us-central1 |
| `edge-net-dashboard` | `https://edge-net-dashboard-875130704813.us-central1.run.app` | Cloud Run, us-central1 |
| `edge-net-genesis` | `https://edge-net-genesis-875130704813.us-central1.run.app` | Cloud Run, us-central1 |
| `edge-net-relay` | `https://edge-net-relay-875130704813.us-central1.run.app` | Cloud Run, us-central1 |
## 2. Decision
Integrate edge-net's distributed compute network with pi brain's knowledge substrate. Edge-net nodes become first-class brain contributors: they earn rUv for compute that powers embedding generation, vector similarity search, MinCut partitioning, and federated LoRA aggregation. The brain gains horizontally scalable compute; edge-net gains access to curated knowledge and a concrete economic purpose for contributed cycles.
The integration uses the edge-net relay as a bridge between browser WASM nodes and the brain's REST/MCP APIs. Edge-net's existing `WasmMcpServer` speaks MCP and can proxy requests to the brain's SSE MCP transport. Pi-Key identity maps to the brain's contributor pseudonym system (SHAKE-256 derivation) so that edge contributions are attributed and auditable.
## 3. Architecture
### 3.1 Integrated System Diagram
```
Browser Nodes (WASM) Cloud Run Services
==================== ==================
+-----------+ +-----------+ +-----------+
| edge-net | | edge-net | | edge-net |
| node A | | node B | | node C | Contributors
| (browser) | | (browser) | | (browser) | earn rUv for
+-----+-----+ +-----+-----+ +-----+-----+ brain compute
| | |
| WebSocket | WebSocket |
+-------+-------+-------+-------+
| +-----------------+
+------+------+ | edge-net-genesis|
| edge-net | rUv ledger sync | (Cloud Run) |
| relay +----------------------->| QDAG ledger |
| (Cloud Run) | | Node registry |
+------+------+ +-----------------+
|
| Brain API bridge
| (REST or SSE MCP)
|
+------+------+ +-----------------+
| ruvbrain | | edge-net |
| (pi brain) | | dashboard |
| pi.ruv.io | | (React, Cloud |
| | | Run) |
| Knowledge | +-----------------+
| graph, LoRA |
| consensus, |
| embeddings |
+-------------+
```
### 3.2 Data Flow: Brain Operation Distributed to Edge
```
1. Brain receives search query via MCP or REST
|
2. Brain decomposes into distributable subtasks
|
3. Subtasks sent to relay as compute tasks
|
4. Relay fans out to available edge nodes
|
5. Edge nodes execute (HNSW search, embedding, etc.)
|
6. Results return through relay to brain
|
7. Brain aggregates, applies quality gating
|
8. Edge nodes credited rUv via genesis ledger
```
### 3.3 MCP Protocol Bridge
Edge-net's `WasmMcpServer` already implements the MCP JSON-RPC protocol with tools like `vector_search`, `generate_embedding`, `credit_balance`, and `coherence_stats`. The brain exposes 22 tools via SSE MCP at `/sse`. The relay bridges these two MCP surfaces:
```
Browser WASM Node Relay Pi Brain
+-----------------+ +-------------------+ +------------------+
| WasmMcpServer | | | | SSE MCP Server |
| (MessagePort) |--->| WebSocket in | | /sse endpoint |
| | | MCP JSON-RPC out |--->| 22 tools |
| Tools: | | | | |
| - vector_search | | Maps edge tools | | Tools: |
| - embedding | | to brain tools | | - brain_search |
| - lora_forward | | + rUv accounting | | - brain_share |
| - credit_balance| | | | - brain_sync |
+-----------------+ +-------------------+ +------------------+
```
## 4. Integration Points
### 4.1 Identity Mapping
Edge-net uses Pi-Key identity (Ed25519 keys, 314-bit Pi-sized). The brain uses SHAKE-256 contributor pseudonyms derived from API keys. The integration maps between them:
- Edge node's Pi-Key public key is registered with the brain as a contributor identity
- The relay derives a brain-compatible pseudonym from the Pi-Key using SHAKE-256 over the Ed25519 public key bytes
- All brain operations from edge nodes carry this derived pseudonym for attribution
- Witness chains in the brain record the Pi-Key signature alongside the pseudonym
### 4.2 Knowledge Synchronization
Edge-net's collective memory (hippocampal replay, HNSW-indexed patterns) synchronizes with the brain's knowledge graph:
| Edge-Net Component | Brain Component | Sync Direction |
|-------------------|-----------------|----------------|
| `CollectiveMemory` patterns | Brain memories (knowledge graph) | Bidirectional |
| `EntropyConsensus` decisions | Brain quality scores (Bayesian) | Edge -> Brain |
| `NetworkLearning` trajectories | Brain `LearnedPattern` entries | Edge -> Brain |
| MicroLoRA adapter weights | Brain LoRA consensus weights | Brain -> Edge |
| HNSW index state | Brain embedding space | Brain -> Edge (delta sync) |
### 4.3 Entropy Consensus for Quality Voting
Edge-net's entropy-based consensus (Shannon entropy minimization with DeGroot belief mixing) provides a distributed quality signal for brain knowledge. When multiple edge nodes encounter the same brain memory during task execution, their independent quality assessments converge via entropy consensus. The consensus result feeds back to the brain as a distributed Bayesian update, supplementing individual `brain_vote` calls with collective judgment.
### 4.4 RAC Coherence Integration
Edge-net's adversarial coherence engine (RAC, 12 axioms) protects the integrity of shared patterns. When edge nodes share patterns derived from brain knowledge, RAC ensures:
- **Axiom 6 (Disagreement is signal)**: Conflicting edge assessments of brain knowledge trigger investigation
- **Axiom 9 (Quarantine is mandatory)**: Suspicious patterns from untrusted nodes are quarantined before reaching the brain
- **Axiom 11 (Equivocation detectable)**: Merkle tree audit trail prevents nodes from submitting contradictory results
## 5. Distributed Compute Tasks
Brain operations that can be farmed out to edge-net WASM nodes:
### 5.1 Vector Similarity Search
The brain's HNSW index can be partitioned across edge nodes. Each node holds a shard of the index and executes approximate nearest-neighbor queries locally. The relay collects top-K results from multiple shards and merges them.
- **Edge capability**: `HnswIndex` in `edge-net/src/ai/memory.rs` with 150x speedup over naive search
- **WASM SIMD**: `simd128` intrinsics accelerate cosine distance on supporting browsers
- **Shard strategy**: Brain partitions index by domain/namespace; each edge node caches its assigned shard
### 5.2 Embedding Generation
The brain's `structured_hash_features()` generates lexical n-gram embeddings. The MicroLoRA transform adds learned semantic refinement. Both stages can run on edge nodes:
- **Stage 1 (lexical)**: `structured_hash_features()` is pure computation on text input, runs entirely in WASM
- **Stage 2 (LoRA transform)**: MicroLoRA forward pass with rank 1-16, `<50us` for rank-1 on edge-net's `AdapterPool`
- **Consensus weights**: Edge nodes pull the latest LoRA consensus from the brain, ensuring embedding consistency
### 5.3 MinCut Graph Partitioning
The brain's `SubpolynomialMinCut` algorithm for knowledge graph partitioning can be parallelized:
- **Partition strategy**: Each edge node processes a subgraph assigned by the brain
- **Merge**: The relay collects partial cuts and the brain computes the global minimum
- **Use case**: When the knowledge graph grows large, rebalancing partitions benefits from distributed compute
### 5.4 Federated LoRA Training
Edge-net already implements federated learning with Byzantine-tolerant gradient gossip (`GradientGossip` in `edge-net/src/ai/federated.rs`):
- **TopK sparsification**: 90% gradient compression reduces bandwidth
- **Byzantine detection**: 2-sigma outlier exclusion removes malicious gradients
- **Differential privacy**: Gaussian noise injection protects individual contributions
- **Integration**: Edge nodes train MicroLoRA weights on local task patterns; brain aggregates via reputation-weighted federated averaging
### 5.5 Quality Voting
Distributed Bayesian quality updates from edge nodes:
- Each edge node that uses a brain memory during task execution records success/failure
- Results are aggregated via entropy consensus across participating edge nodes
- The consensus vote is submitted to the brain as a weighted Bayesian update
- Brain's quality gating (auto-archive at `quality_score.mean() < 0.3` after 5 observations) benefits from higher observation counts
## 6. Security Model
### 6.1 Identity and Authentication
| Layer | Mechanism | Purpose |
|-------|-----------|---------|
| Edge node identity | Pi-Key Ed25519 (314-bit) | Per-node cryptographic identity |
| Brain contributor pseudonym | SHAKE-256 of Pi-Key public key | Brain-compatible attribution |
| Operation signing | Ed25519 signatures on task results | Non-repudiation |
| Brain witness chains | Append-only signed chain | Audit trail for all contributions |
### 6.2 Byzantine Fault Tolerance
Edge nodes are untrusted by default. The system provides multiple layers of protection:
- **Gradient poisoning**: `ByzantineDetector` in federated.rs excludes gradients beyond 2 standard deviations from the reputation-weighted mean
- **Result validation**: Brain cross-checks edge search results against local index for random samples (probabilistic verification)
- **Reputation decay**: Nodes that submit invalid results lose reputation, reducing their influence on future aggregation
- **RAC quarantine**: Patterns from low-reputation nodes enter quarantine before affecting brain state
### 6.3 Sybil Resistance
Sybil resistance uses layered defenses instead of high staking barriers, ensuring the network remains accessible to newcomers while protecting against abuse:
| Defense Layer | Mechanism | Purpose |
|---------------|-----------|---------|
| Proof of Work | First contribution requires completing 1 compute task | Proves real compute capacity; blocks zero-cost identity creation |
| Rate Limiting | 100 writes/hour per identity | Bounds the damage any single Sybil identity can cause |
| Quality Gating | Shared knowledge must pass RAC coherence check (score > 0.5) | Prevents low-quality spam from polluting the brain |
| Progressive Trust | Higher reputation tiers unlock higher rate limits and priority | Long-term good behavior is expensive to fake across many identities |
| Reputation Decay | 1% decay per epoch (see `ReputationCurve.apply_decay()`) | Abandoned Sybil identities lose influence automatically |
Staking remains available as an optional mechanism (see Section 8.3) but is not required for basic participation. Nodes that submit invalid results (as detected by probabilistic verification or Byzantine detection) have their reputation slashed, which is more effective than financial slashing alone because reputation takes sustained effort to rebuild.
### 6.4 Data Privacy
- Edge nodes receive only the data needed for their assigned subtask (need-to-know)
- Search queries are decomposed so no single edge node sees the full query context
- LoRA gradients are protected by differential privacy (Gaussian noise with configurable epsilon)
- The relay strips personally identifiable metadata before forwarding to edge nodes
## 7. API Bridge Design
Three integration options, ordered by implementation simplicity:
### Option A: Direct REST (recommended for Phase 1)
Edge-net nodes call the brain's REST API directly through the relay proxy.
```
Edge Node --[WS]--> Relay --[HTTPS]--> pi.ruv.io/v1/memories/search
pi.ruv.io/v1/memories (POST)
pi.ruv.io/v1/lora/latest
```
- Simplest to implement: relay proxies HTTP requests with Pi-Key authentication headers
- Each edge node's requests are independently rate-limited at the relay
- No persistent connection required between relay and brain
### Option B: Relay-Proxied Batch (recommended for Phase 2)
The relay batches requests from multiple edge nodes into bulk brain API calls.
```
Edge Nodes --[WS]--> Relay --[HTTPS bulk]--> pi.ruv.io/v1/batch
```
- Reduces brain API call count when many edge nodes query simultaneously
- Relay aggregates search queries, deduplicates, and fans results back to requesting nodes
- Requires a `/v1/batch` endpoint on the brain (new development)
### Option C: SSE MCP Bridge (recommended for Phase 3)
The relay maintains a persistent SSE MCP session with the brain and multiplexes edge node requests over it.
```
Edge Nodes --[WS/MCP]--> Relay --[SSE MCP]--> pi.ruv.io/sse
```
- Full tool access: all 22 brain MCP tools available to edge nodes
- Persistent connection amortizes SSE setup overhead
- Relay maintains a single SSE session per brain region, multiplexing edge requests
- Requires MCP session management in the relay (handles reconnection, session affinity)
## 8. Economic Model
The economic model is designed around two principles: **accessible** (no barriers to entry) and **sustainable** (finite supply with controlled inflation). The previous model required 10-200 rUv staking to participate, creating a chicken-and-egg problem for new users. The revised model eliminates cost barriers entirely and uses contribution rewards to bootstrap the economy.
### 8.1 Free-to-Read, Earn-to-Write
All read operations are FREE. No rUv cost, no staking requirement. This removes the single biggest barrier to adoption. Write operations are also free to perform but earn rUv rewards when quality thresholds are met.
| Operation | Cost | Reward | Conditions |
|-----------|------|--------|------------|
| Search brain | FREE | 0 rUv | -- |
| Get brain status | FREE | 0 rUv | -- |
| List memories | FREE | 0 rUv | -- |
| Share knowledge | FREE | 2 rUv | Quality score > 0.5 after RAC review |
| Vote on quality | FREE | 0.1 rUv | Must have completed >= 1 task |
| Generate embedding | FREE | 1 rUv | Result passes hash verification |
| LoRA gradient | FREE | 5 rUv | Gradient passes Byzantine detection |
| WASM compute | FREE | 0.5-3 rUv | Based on compute time and success rate |
Implementation reference: rewards are credited via `WasmCreditLedger.credit()` in `examples/edge-net/src/credits/mod.rs`. The ledger's CRDT merge (`WasmCreditLedger.merge()`) ensures consistency across P2P nodes.
### 8.2 Contribution Curve (Revised)
The contribution curve rewards early adopters without being extractive. The formula is updated from the existing `ContributionCurve` in `examples/edge-net/src/credits/mod.rs`:
```
multiplier = FLOOR + (MAX_BONUS - FLOOR) * e^(-network_compute / DECAY_CONSTANT)
```
| Parameter | Old Value | New Value | Rationale |
|-----------|-----------|-----------|-----------|
| `MAX_BONUS` | 10x | 5x | Still rewards early adopters, less extractive |
| `FLOOR_MULTIPLIER` | 1x (implicit) | 0.5x | Even mature network still pays something |
| `DECAY_CONSTANT` | 1,000,000 CPU-hours | 1,000,000 CPU-hours | Unchanged |
At genesis (0 compute-hours), multiplier is 5x. As total network compute grows, multiplier decays toward 0.5x. This means:
| Network Compute | Multiplier |
|----------------|------------|
| 0 hours (genesis) | 5.0x |
| 100K hours | 4.6x |
| 500K hours | 2.9x |
| 1M hours | 1.9x |
| 5M hours | 0.5x |
The `ContributionCurve::current_multiplier()` implementation must be updated to use the revised constants. The `FLOOR_MULTIPLIER` ensures contributors always earn something, even when the network is mature.
### 8.3 Reputation Tiers (Revised)
Reputation tiers use the existing `ReputationTier` enum from `examples/edge-net/src/economics/reputation.rs` but add a Newcomer tier and make staking optional:
| Tier | Score | Reward Mult | Staking Required | Access |
|------|-------|-------------|-----------------|--------|
| Newcomer | 0-10 | 0.5x | None | Read-only free, writes earn at 0.5x |
| Bronze | 10-25 | 1.0x | 0 rUv | Full read/write, standard rewards |
| Silver | 25-50 | 1.1x | 10 rUv (optional) | Priority task allocation |
| Gold | 50-75 | 1.25x | 50 rUv (optional) | Price discounts via `ReputationCurve.discount()`, priority |
| Platinum | 75-100 | 1.5x | 100 rUv (optional) | Max discounts, governance voting weight |
Key changes from the previous model:
- **Staking is OPTIONAL.** It provides benefits (price discounts, governance weight) but is not required for basic participation. This eliminates the chicken-and-egg problem where new users need rUv to participate but cannot earn rUv without participating.
- **Newcomer tier added.** Score 0-10 earns at 0.5x rate, providing immediate earning capability.
- **Reward multipliers** are applied via `ReputationTier::reward_multiplier()` which already implements the 1.0x/1.1x/1.25x/1.5x tiers in the codebase.
### 8.4 Halving Schedule
Brain rewards halve every 100K brain operations (cumulative across all nodes). This creates a Bitcoin-like deflationary schedule that ensures finite total rUv supply:
| Epoch | Cumulative Operations | Base Reward Multiplier |
|-------|-----------------------|----------------------|
| 0 | 0 - 100K | 1.0x |
| 1 | 100K - 200K | 0.5x |
| 2 | 200K - 400K | 0.25x |
| 3 | 400K - 800K | 0.125x |
| N | ... | 1/(2^N)x |
The halving multiplier stacks with the contribution curve and reputation tier multipliers:
```
effective_reward = base_reward * contribution_multiplier * tier_multiplier * halving_multiplier
```
This creates urgency for early participation (higher halving multiplier) without the aggressive 10x genesis bonus that the old contribution curve provided.
### 8.5 Protocol Budget
Each epoch has a fixed rUv budget to prevent runaway inflation:
| Parameter | Value | Rationale |
|-----------|-------|-----------|
| Initial epoch budget | 1,000,000 rUv | Sufficient for ~200K reward operations at average 5 rUv |
| Budget carry-over | Yes | Unspent budget rolls to next epoch |
| Budget exhaustion behavior | Rewards pause until next epoch | Hard cap prevents inflation |
| Epoch duration | 7 days or 100K operations, whichever comes first | Bounds both time and volume |
The protocol fund is managed by `EconomicEngine.get_protocol_fund()`. The `ComputeAMM` in `examples/edge-net/src/economics/amm.rs` provides secondary revenue through trading fees (0.3%-3% dynamic fee based on pool utilization) that supplement the protocol budget after the bootstrap phase.
### 8.6 Sybil Resistance (Economic)
Instead of requiring high staking barriers (the old model required 10-200 rUv to participate), the revised model uses layered economic defenses:
| Defense | Mechanism | Implementation |
|---------|-----------|----------------|
| Proof of Work | First contribution requires completing 1 compute task | Proves real compute capacity; blocks zero-cost Sybil creation |
| Rate limiting | 100 writes/hour per identity | Bounds damage per identity; implemented at relay level |
| Quality gating | Shared knowledge must pass RAC coherence check | `AdversarialCoherence` score > 0.5 required |
| Progressive trust | Higher tiers unlock higher rate limits and priority | `ReputationCurve.select_nodes_for_task()` weights by reputation |
| Reputation cost | Reputation takes sustained effort to build (diminishing returns in `record_task()`) | More expensive to maintain many Sybil identities than one real identity |
### 8.7 Sustainability Analysis
The revised model is sustainable because total rUv supply converges:
**Finite supply proof.** With halving schedule and fixed epoch budget:
- Epoch 0: up to 1,000,000 rUv minted
- Epoch 1: up to 500,000 rUv
- Epoch N: up to 1,000,000 / 2^N rUv
- Total maximum supply = 1,000,000 * sum(1/2^N for N=0..inf) = 2,000,000 rUv
**Revenue sources by phase:**
| Phase | Duration | Primary Revenue | Secondary Revenue |
|-------|----------|----------------|-------------------|
| Bootstrap (0-10K nodes) | Months 1-6 | Protocol fund subsidy | None |
| Growth (10K-100K nodes) | Months 6-18 | Protocol fund + AMM fees | Brain API consumer payments |
| Self-sustaining (100K+ nodes) | Month 18+ | AMM fees + consumer payments | Liquidity provider fees |
**Deflationary pressure.** As brain knowledge grows, rUv utility increases (more valuable knowledge to access, more compute tasks available). The AMM's constant-product formula (`x * y = k` in `ComputeAMM`) ensures that as demand for compute grows, the rUv price of compute increases, creating natural deflationary pressure.
**Protocol fund sufficiency.** At 2,000,000 rUv maximum supply and projected 50K operations/month at maturity, the protocol fund sustains rewards for 24+ months even without secondary revenue. AMM fees (0.3%-3% of all swaps, tracked via `ComputeAMM.fees_collected`) provide a sustainable revenue stream after bootstrap.
## 9. Implementation Phases
### Phase 1: REST Bridge (Weeks 1-3)
1. Add Pi-Key to brain pseudonym derivation in the relay
2. Implement relay HTTP proxy to brain REST API with rate limiting
3. Add `brain_search` and `brain_share` proxy commands to edge-net's `WasmMcpServer`
4. Edge nodes can search brain knowledge and share patterns via relay
5. rUv accounting for search and share operations
### Phase 2: Distributed Search (Weeks 4-6)
1. Brain partitions HNSW index into shards by namespace
2. Relay distributes shard assignments to edge nodes
3. Edge nodes cache assigned shards and execute local HNSW queries
4. Relay merges top-K results from multiple edge nodes
5. Probabilistic verification: brain spot-checks 5% of edge search results
### Phase 3: Federated Intelligence (Weeks 7-10)
1. SSE MCP bridge between relay and brain
2. Edge nodes pull brain LoRA consensus weights and generate embeddings locally
3. Federated LoRA training: edge nodes contribute gradients from local task patterns
4. Entropy consensus quality voting feeds back to brain Bayesian scores
5. Collective memory synchronization between edge-net and brain knowledge graph
## 10. Monitoring
### 10.1 Integration Metrics
| Metric | Source | Alert Threshold |
|--------|--------|-----------------|
| Edge-to-brain request latency (p99) | Relay logs | > 500ms |
| Brain search delegation rate | Brain metrics | < 10% (not using edge) |
| Edge node participation rate | Genesis ledger | < 50 active nodes |
| Byzantine rejection rate | Relay metrics | > 5% of submissions |
| rUv earned per edge node (daily avg) | Genesis ledger | < 1 rUv (nodes leaving) |
| LoRA consensus drift | Brain LoRA metrics | Cosine distance > 0.1 from last epoch |
### 10.2 Dashboard Integration
The existing `edge-net-dashboard` (React, Cloud Run) is extended with:
- Brain integration status panel (relay connection health, brain API availability)
- Per-node brain contribution metrics (searches executed, embeddings generated, rUv earned)
- Network-wide federated learning progress (consensus convergence, gradient acceptance rate)
- Knowledge flow visualization (patterns shared edge -> brain, knowledge pulled brain -> edge)
## 11. Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Edge nodes return stale search results from outdated shards | Degraded search quality | Shard version headers; brain rejects results from outdated shards |
| Relay becomes single point of failure | Edge-brain integration offline | Deploy relay in multiple regions; edge nodes failover to direct brain REST |
| LoRA gradient poisoning from compromised edge nodes | Corrupted brain consensus | Byzantine detection (2-sigma exclusion), reputation-weighted aggregation, differential privacy |
| rUv inflation from brain subsidies | Devalued rUv economy | Fixed protocol fund budget per epoch; halving schedule aligned with genesis sunset |
| High browser compute costs deter edge participation | Low node count | Battery-aware throttling, configurable CPU limits (10-50%), clear rUv earning visibility |
| Brain API rate limits block edge relay | Throttled integration | Relay batches requests; brain allowlists relay IP with higher rate limits |
| Privacy leak through search query distribution | User data exposure | Query decomposition, differential privacy on inputs, need-to-know distribution |
## 12. Acceptance Criteria
### Phase 1
- [ ] Edge node can execute `brain_search` through relay and receive results
- [ ] Edge node can execute `brain_share` to publish a pattern to brain knowledge graph
- [ ] Pi-Key identity correctly maps to brain contributor pseudonym
- [ ] rUv credited for search and share operations via genesis ledger
- [ ] Rate limiting prevents single edge node from overwhelming brain API
### Phase 2
- [ ] Brain HNSW index partitioned into at least 4 shards
- [ ] Edge nodes cache assigned shards and return search results within 200ms
- [ ] Merged results from edge nodes match single-origin results at 95% recall
- [ ] Probabilistic verification catches intentionally wrong results > 90% of the time
- [ ] Dashboard shows per-node search contribution metrics
### Phase 3
- [ ] Edge nodes pull LoRA consensus and generate embeddings consistent with brain
- [ ] Federated gradient gossip produces consensus within 5% cosine distance of centralized training
- [ ] Entropy consensus quality votes appear in brain quality scores within one epoch
- [ ] Collective memory sync operates bidirectionally without data loss
- [ ] SSE MCP bridge maintains persistent connection with automatic reconnection
### Economics
- [ ] New users can search brain without any rUv balance or staking
- [ ] Newcomer tier (score 0-10) earns rUv at 0.5x rate
- [ ] `ContributionCurve` updated with `MAX_BONUS = 5.0` and `FLOOR_MULTIPLIER = 0.5`
- [ ] Halving schedule reduces base reward multiplier after every 100K cumulative operations
- [ ] Protocol budget limits per-epoch rUv minting to 1,000,000 rUv
- [ ] Staking is optional (not required for basic participation at Newcomer or Bronze tiers)
- [ ] Rate limiting enforced at 100 writes/hour per identity
- [ ] Quality gating requires RAC coherence score > 0.5 for shared knowledge
- [ ] AMM fee revenue tracked and reported on dashboard
## 13. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-059 | Shared Brain Google Cloud -- brain deployment architecture that edge-net integrates with |
| ADR-060 | Shared Brain Capabilities -- federated MicroLoRA and knowledge substrate that edge-net extends |
| ADR-062 | Brainpedia Architecture -- knowledge pages that edge-net quality voting improves |
| ADR-063 | WASM Executable Nodes -- WASM node system that edge-net browser nodes share technology with |
| ADR-064 | Pi Brain Infrastructure -- Cloud Run deployment, custom domains, persistence layer |
| ADR-066 | SSE MCP Transport -- the MCP transport that the Phase 3 bridge connects to |

View file

@ -0,0 +1,344 @@
# ADR-070: npx ruvector Unified Integration
**Status**: Proposed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-065 (npm Publishing Strategy), ADR-064 (Pi Brain Infrastructure), ADR-066 (SSE MCP Transport), ADR-069 (Edge-Net Integration)
## 1. Context
The RuVector npm ecosystem has grown to 55+ packages across multiple concerns:
- **`ruvector`** (v0.1.100): Core vector database with native/WASM/RVF backend auto-detection, CLI (`npx ruvector`), GNN wrappers, SONA embeddings, ONNX, parallel intelligence
- **`@ruvector/pi-brain`**: Pi brain CLI + SDK + MCP stdio proxy for the shared intelligence at `pi.ruv.io`
- **`@ruvector/edge-net`**: Distributed P2P browser compute network (WASM, Web Workers, rUv credits)
- **50+ other packages**: solver, rvf, gnn, attention, sona, ruvllm, tiny-dancer, ospipe, etc.
These systems are independently installable and operate in isolation. There is no unified entry point that lets a user:
1. Access the shared brain from the CLI
2. Join the edge compute network from Node.js
3. Start an MCP server that bridges all capabilities
4. Manage their π identity and rUv credits from one place
The `ruvector` CLI already has a `commander`-based command structure with subcommands for vector operations (insert, search, benchmark, etc.). Extending it to include brain, edge, and MCP integration creates a single `npx ruvector` entry point for the entire ecosystem.
## 2. Decision
Extend the existing `ruvector` CLI with three new command groups: `brain`, `edge`, and `mcp`. These commands call into `@ruvector/pi-brain` and `@ruvector/edge-net` as optional peer dependencies — they are lazy-loaded only when invoked, so the core `ruvector` package remains lightweight.
```
npx ruvector brain share "JWT refresh pattern" --category pattern
npx ruvector brain search "auth" --limit 10
npx ruvector edge status
npx ruvector edge join --contribution 0.3
npx ruvector mcp start --transport sse --url https://pi.ruv.io
npx ruvector identity generate
npx ruvector identity show
```
## 3. Architecture
### 3.1 Command Hierarchy
```
npx ruvector
├── insert (existing) Vector insert
├── search (existing) Vector search
├── benchmark (existing) Performance benchmark
├── info (existing) System info
│
├── brain (NEW) Shared intelligence
│ ├── share Share knowledge
│ ├── search Semantic search
│ ├── get Retrieve by ID
│ ├── vote Quality vote
│ ├── list List memories
│ ├── delete Delete own
│ ├── transfer Domain transfer
│ ├── drift Check drift
│ ├── partition Knowledge topology
│ ├── status System health
│ ├── sync LoRA weight sync
│ ├── page Brainpedia CRUD
│ └── node WASM node publish
│
├── edge (NEW) Distributed compute
│ ├── status Network status (genesis, relay, nodes)
│ ├── join Join as compute node
│ ├── balance Check rUv balance
│ ├── tasks List available compute tasks
│ └── dashboard Open dashboard URL
│
├── mcp (NEW) MCP server management
│ ├── start Start MCP server (stdio or SSE)
│ ├── tools List available MCP tools
│ └── test Test MCP connection
│
└── identity (NEW) π identity management
├── generate Generate new π key
├── show Display current pseudonym
├── export Export key for backup
└── import Import key from backup
```
### 3.2 Dependency Strategy
```
ruvector (core)
├── commander, chalk, ora (bundled)
├── @ruvector/core (optional - native backend)
├── @ruvector/rvf (optional - RVF backend)
│
├── @ruvector/pi-brain (optional peer dep - brain commands)
│ └── @modelcontextprotocol/sdk
│
└── @ruvector/edge-net (optional peer dep - edge commands)
└── wasm-bindgen (WASM runtime)
```
When a user runs `npx ruvector brain search "auth"` without `@ruvector/pi-brain` installed, the CLI prints:
```
Edge command requires @ruvector/pi-brain. Install with:
npm install @ruvector/pi-brain
```
This keeps the core package at ~2MB while allowing the full ecosystem to be progressively adopted.
### 3.3 Environment Configuration
All commands read from a unified config hierarchy:
| Source | Priority | Example |
|--------|----------|---------|
| CLI flags | 1 (highest) | `--url https://pi.ruv.io` |
| Environment vars | 2 | `PI=key`, `BRAIN_URL=url` |
| `.env` file | 3 | `PI=abc123...` in project root |
| `~/.ruvector/config.json` | 4 | Global config |
| Defaults | 5 (lowest) | `https://pi.ruv.io` |
The π key (`PI` env var) is shared across brain, edge, and MCP commands. One identity, one key, three systems.
### 3.4 Identity Derivation Chain
```
User's π key (64 hex chars)
│
├── SHAKE-256(key) ──► Brain pseudonym (contributor ID)
│ Used for: brain share, vote, delete
│
├── Ed25519(key) ────► Edge Pi-Key (node identity)
│ Used for: edge join, rUv transactions
│
└── HMAC-SHA256(key, "mcp") ──► MCP session token
Used for: SSE MCP auth
```
A single key derives three identities through different cryptographic paths. The SHAKE-256 path matches the brain server's `auth.rs` pseudonym derivation. The Ed25519 path matches edge-net's `pikey` module. The HMAC path provides MCP session auth.
## 4. New CLI Commands
### 4.1 `ruvector brain`
Wraps `@ruvector/pi-brain`'s `PiBrainClient`:
```typescript
// Lazy load
const { PiBrainClient } = await import('@ruvector/pi-brain');
const client = new PiBrainClient({ url: opts.url, key: opts.key });
```
| Command | Maps to | Description |
|---------|---------|-------------|
| `brain share <title> -c <category> -t <tags>` | `POST /v1/memories` | Share knowledge |
| `brain search <query> -l <limit>` | `GET /v1/memories/search` | Semantic search |
| `brain get <id>` | `GET /v1/memories/:id` | Retrieve with provenance |
| `brain vote <id> <up\|down>` | `POST /v1/memories/:id/vote` | Quality vote |
| `brain list [-c category] [-l limit]` | `GET /v1/memories/list` | List memories |
| `brain delete <id>` | `DELETE /v1/memories/:id` | Delete own contribution |
| `brain transfer <source> <target>` | `POST /v1/transfer` | Domain transfer |
| `brain drift [--domain <d>]` | `GET /v1/drift` | Drift detection |
| `brain partition [--domain <d>]` | `GET /v1/partition` | Knowledge topology |
| `brain status` | `GET /v1/status` | System health |
| `brain sync [pull\|push\|both]` | `POST /v1/lora/submit` | LoRA sync |
### 4.2 `ruvector edge`
Wraps `@ruvector/edge-net` for Node.js (non-browser) usage:
| Command | Description |
|---------|-------------|
| `edge status` | Query genesis node for network stats, rUv supply, sunset phase |
| `edge join --contribution 0.3` | Join as compute node (headless, Node.js Web Worker polyfill) |
| `edge balance` | Check rUv balance for current identity |
| `edge tasks` | List available distributed compute tasks |
| `edge dashboard` | Open edge-net dashboard in browser |
Edge commands hit the deployed services:
- Genesis: `https://edge-net-genesis-875130704813.us-central1.run.app`
- Relay: `https://edge-net-relay-875130704813.us-central1.run.app`
- Dashboard: `https://edge-net-dashboard-875130704813.us-central1.run.app`
### 4.3 `ruvector mcp`
Manages MCP server lifecycle:
| Command | Description |
|---------|-------------|
| `mcp start` | Start stdio MCP server (default, for `claude mcp add`) |
| `mcp start --transport sse --port 8080` | Start SSE MCP server locally |
| `mcp tools` | List all 22 available MCP tools |
| `mcp test` | Send test JSON-RPC to verify connection |
The `mcp start` command replaces `cargo run -p mcp-brain` for users who don't have Rust installed:
```bash
# Before (requires Rust toolchain)
claude mcp add pi-brain -- cargo run -p mcp-brain
# After (just Node.js)
claude mcp add pi-brain -- npx ruvector mcp start
```
### 4.4 `ruvector identity`
Manages the π key:
| Command | Description |
|---------|-------------|
| `identity generate` | Generate new π key, display, copy to clipboard |
| `identity show` | Show current key's pseudonym, edge Pi-Key, reputation |
| `identity export` | Export key to file (encrypted with passphrase) |
| `identity import <file>` | Import key from encrypted backup |
## 5. Implementation
### 5.1 File Changes
| File | Change |
|------|--------|
| `npm/packages/ruvector/bin/cli.js` | Add `brain`, `edge`, `mcp`, `identity` command groups |
| `npm/packages/ruvector/package.json` | Add `@ruvector/pi-brain` and `@ruvector/edge-net` as optional peer deps |
| `npm/packages/ruvector/src/commands/brain.ts` | Brain command handlers with lazy `pi-brain` import |
| `npm/packages/ruvector/src/commands/edge.ts` | Edge command handlers with lazy `edge-net` import |
| `npm/packages/ruvector/src/commands/mcp.ts` | MCP server start/test with transport selection |
| `npm/packages/ruvector/src/commands/identity.ts` | Key generation, derivation, export/import |
### 5.2 Lazy Loading Pattern
```typescript
async function requirePiBrain(): Promise<typeof import('@ruvector/pi-brain')> {
try {
return await import('@ruvector/pi-brain');
} catch {
console.error(chalk.red('Brain commands require @ruvector/pi-brain'));
console.error(chalk.yellow(' npm install @ruvector/pi-brain'));
process.exit(1);
}
}
```
### 5.3 Output Formatting
All commands output JSON by default when piped (`!process.stdout.isTTY`) and human-readable tables/colors when interactive. The `--json` flag forces JSON output.
```bash
# Human-readable
npx ruvector brain status
# Memories: 42 | Contributors: 7 | Quality: 0.82 | Drift: stable
# Machine-readable
npx ruvector brain status --json
# {"total_memories":42,"total_contributors":7,...}
# Piped
npx ruvector brain search "auth" | jq '.[] | .title'
```
## 6. Security
### 6.1 Key Storage
The π key is never stored in plaintext on disk by the CLI. Options:
- Environment variable (`PI=...`)
- `.env` file (user's responsibility to gitignore)
- System keychain via `keytar` (optional dependency)
- Encrypted file via `identity export/import`
### 6.2 Network Security
All CLI commands communicate over HTTPS. The brain client validates TLS certificates. No HTTP fallback.
### 6.3 Dependency Isolation
Brain and edge dependencies are optional peers, not bundled. This prevents supply chain attacks through transitive dependencies from affecting users who only use the core vector database.
## 7. Versioning
The `ruvector` CLI version is independent of the brain and edge package versions. The CLI detects compatible version ranges at runtime:
```typescript
const pkg = await import('@ruvector/pi-brain/package.json');
if (!semver.satisfies(pkg.version, '>=0.1.0')) {
console.warn(chalk.yellow(`pi-brain ${pkg.version} may not be compatible`));
}
```
## 8. Testing
| Test | Description |
|------|-------------|
| `brain` commands without `pi-brain` installed | Graceful error with install instructions |
| `brain status` with mock server | Returns formatted status |
| `brain search "query"` with live backend | Returns results |
| `edge status` against genesis | Returns network stats |
| `mcp start` stdio | Responds to JSON-RPC initialize |
| `mcp tools` | Lists 22 tools |
| `identity generate` | Produces valid 64-char hex key |
| `identity show` | Derives correct SHAKE-256 pseudonym |
| JSON output mode | All commands produce valid JSON with `--json` |
| Pipe detection | Auto-JSON when stdout is not TTY |
## 9. Migration Path
### Phase 1: CLI Extension (1 week)
Add command groups to `bin/cli.js`. Wire `brain` commands to `@ruvector/pi-brain`. Add `identity` commands with key generation and SHAKE-256 derivation.
### Phase 2: Edge Integration (1 week)
Add `edge` commands. Create Node.js adapter for `@ruvector/edge-net` (which targets browsers). Implement headless join for server-side compute contribution.
### Phase 3: MCP Proxy (1 week)
Add `mcp start` with stdio and SSE transport support. Replace `cargo run -p mcp-brain` as the recommended MCP setup for non-Rust users.
### Phase 4: Publish (1 week)
Bump `ruvector` to 0.2.0. Update README. Publish `@ruvector/pi-brain` and ensure version compatibility. Update landing page docs.
## 10. Consequences
### Positive
- Single entry point: `npx ruvector` provides access to vector DB, shared brain, edge compute, and MCP
- Progressive adoption: core package stays lightweight, features opt-in via peer deps
- No Rust required: `npx ruvector mcp start` replaces `cargo run -p mcp-brain`
- Unified identity: one π key for all three systems
### Negative
- CLI complexity increases — more surface area to maintain
- Optional peer deps can confuse users (unclear what's installed)
- Node.js adapter for edge-net (browser-targeted WASM) may have compatibility gaps
### Neutral
- Version coordination between `ruvector`, `pi-brain`, and `edge-net` requires semver discipline
- Existing `pi-brain` CLI (`npx pi-brain`) continues to work independently
## 11. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-065 | npm Publishing Strategy — package categories, publish order |
| ADR-064 | Pi Brain Infrastructure — Cloud Run deployment, domains |
| ADR-066 | SSE MCP Transport — SSE protocol that `mcp start --transport sse` exposes |
| ADR-069 | Edge-Net Integration — distributed compute that `edge` commands access |
| ADR-059 | Shared Brain Google Cloud — backend that `brain` commands call |
| ADR-060 | Shared Brain Capabilities — 7 capabilities exposed through `brain` subcommands |

View file

@ -0,0 +1,510 @@
# ADR-071: npx ruvector Ecosystem Gap Analysis
**Status**: Proposed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-065 (npm Publishing Strategy), ADR-070 (npx ruvector Unified Integration)
## 1. Context
The ruvector project produces **79 npm packages** and **27 WASM crates** spanning vector databases, graph engines, LLM orchestration, quantum simulation, spiking neural networks, cryptographic primitives, and distributed compute. The primary CLI entry point — `npx ruvector` (v0.1.100) — exposes only a fraction of this surface: vector CRUD, GNN layers, attention mechanisms, and system diagnostics.
Meanwhile, significant capabilities exist as Rust crates with no npm wrapper, as WASM crates with no JavaScript bindings published, or as npm packages that are disconnected from the CLI. This ADR catalogs every gap and proposes a roadmap to make the full ecosystem accessible through `npx ruvector`.
## 2. Current State
### 2.1 What `npx ruvector` Exposes Today
| Command Group | Subcommands | Backend |
|--------------|-------------|---------|
| `create` | Create vector DB | @ruvector/core |
| `insert` | Insert vectors from JSON | @ruvector/core |
| `search` | ANN search with filters | @ruvector/core |
| `stats` | Database statistics | @ruvector/core |
| `benchmark` | Performance benchmarks | @ruvector/core |
| `info` | System info (backends, versions) | built-in |
| `install` | Install optional packages | built-in |
| `doctor` | Health check | built-in |
| `gnn layer` | Create/test GNN layers | @ruvector/gnn |
| `gnn compress` | Adaptive tensor compression | @ruvector/gnn |
| `gnn search` | Differentiable search | @ruvector/gnn |
| `gnn info` | GNN module info | @ruvector/gnn |
| `attention compute` | 5 attention mechanisms | @ruvector/attention |
| `attention benchmark` | Benchmark attention types | @ruvector/attention |
| `attention hyperbolic` | Hyperbolic geometry ops | @ruvector/attention |
| `attention list` | List mechanisms | @ruvector/attention |
| `attention info` | Module details | @ruvector/attention |
**Total: 17 commands across 4 groups.**
### 2.2 What Exists as npm Packages but NOT in the CLI
| npm Package | Version | Capability | CLI Integration |
|-------------|---------|-----------|----------------|
| `@ruvector/pi-brain` | 0.1.0 | Shared brain CLI + SDK + MCP | **Missing** — has own `npx pi-brain` CLI |
| `@ruvector/sona` | 0.1.4 | Self-optimizing neural architecture | Bundled dep but **no CLI commands** |
| `@ruvector/rvf` | 0.2.0 | RuVector Format SDK (read/write/validate) | Optional dep but **no CLI commands** |
| `@ruvector/rvf-solver` | 0.1.7 | Temporal constraint solver | **No CLI** |
| `@ruvector/rvf-wasm` | 0.1.5 | RVF WASM microkernel | **No CLI** |
| `@ruvector/rvf-node` | 0.1.7 | RVF Node.js bindings | **No CLI** |
| `@ruvector/rvf-mcp-server` | 0.1.3 | RVF MCP server | **No CLI** |
| `@ruvector/ruvllm` | 2.5.1 | LLM orchestration + SONA + HNSW | **Separate CLI** (`npx ruvllm`) |
| `@ruvector/ruvllm-cli` | 0.1.0 | LLM inference CLI | **Separate binary** |
| `@ruvector/ruvllm-wasm` | 0.1.0 | WASM LLM inference | **No CLI** |
| `@ruvector/graph-node` | 2.0.2 | Native hypergraph bindings | **No CLI** |
| `@ruvector/graph-wasm` | 2.0.2 | Neo4j-compatible hypergraph WASM | **No CLI** |
| `@ruvector/graph-data-generator` | 0.1.0 | Synthetic graph data generation | **No CLI** |
| `@ruvector/ruqu-wasm` | 2.0.5 | Quantum simulations | **No CLI** |
| `@ruvector/spiking-neural` | 1.0.1 | Spiking neural networks (SIMD) | **No CLI** |
| `@ruvector/ospipe` | 0.1.2 | Personal AI memory | **No CLI** |
| `@ruvector/ospipe-wasm` | 0.1.0 | Personal AI memory WASM | **No CLI** |
| `@ruvector/rvdna` | 0.3.0 | Genomic analysis (20-SNP biomarker) | **No CLI** |
| `@ruvector/scipix` | 0.1.0 | OCR for scientific documents | **No CLI** |
| `@ruvector/tiny-dancer` | 0.1.17 | Neural router (FastGRNN) | **No CLI** |
| `@ruvector/router` | 0.1.28 | Semantic router for AI agents | **No CLI** |
| `@ruvector/ruvbot` | 0.3.1 | Self-learning AI assistant | **Separate CLI** |
| `@ruvector/rvlite` | 0.2.4 | Lightweight DB (SQL/SPARQL/Cypher) | **No CLI** |
| `@ruvector/agentic-integration` | 1.0.0 | Distributed agent coordination | **No CLI** |
| `@ruvector/agentic-synth` | 0.1.6 | Synthetic data generator | **Has own CLI** |
| `@ruvector/burst-scaling` | 1.0.0 | Adaptive burst scaling | **No CLI** |
| `@ruvector/cognitum-gate-wasm` | 0.1.0 | AI coherence gate | **No CLI** |
| `@ruvector/raft` | 0.1.0 | Raft consensus | **No CLI** |
| `@ruvector/replication` | 0.1.0 | Data replication & sync | **No CLI** |
| `@ruvector/postgres-cli` | 0.2.7 | PostgreSQL pgvector CLI | **Separate CLI** |
| `@ruvector/ruvector-extensions` | 0.1.0 | Embeddings, UI, exports, temporal | **No CLI** |
| `@ruvector/ruvector-wasm-unified` | 1.0.0 | Unified TypeScript WASM API | **No CLI** |
**31 packages with no CLI integration.**
### 2.3 What Exists as WASM Crates but NOT as npm Packages
| WASM Crate | Version | Capability | npm Package |
|-----------|---------|-----------|-------------|
| `ruvector-attention-unified-wasm` | 0.1.0 | Unified attention (46 mechanisms) | **Missing** |
| `ruvector-attention-wasm` | — | Attention WASM bindings | Partial (`@ruvector/attention`) |
| `ruvector-dag-wasm` | 0.1.0 | DAG operations | **Missing** |
| `ruvector-delta-wasm` | 0.1.0 | Delta consensus/behavior tracking | **Missing** |
| `ruvector-domain-expansion-wasm` | 0.1.0 | Transfer learning, domain expansion | **Missing** |
| `ruvector-economy-wasm` | 0.1.0 | Economic engine (reputation, AMM) | **Missing** |
| `ruvector-exotic-wasm` | — | Exotic neural architectures | **Missing** |
| `ruvector-fpga-transformer-wasm` | 0.1.0 | FPGA-optimized transformers | **Missing** |
| `ruvector-gnn-wasm` | — | GNN WASM bindings | Partial (`@ruvector/gnn`) |
| `ruvector-graph-transformer-wasm` | — | Graph transformer | **Missing** |
| `ruvector-hyperbolic-hnsw-wasm` | 0.1.0 | Hyperbolic HNSW search | **Missing** |
| `ruvector-learning-wasm` | 0.1.0 | Online learning | **Missing** |
| `ruvector-math-wasm` | — | Math primitives | **Missing** |
| `ruvector-mincut-gated-transformer` | 0.1.0 | MinCut-gated transformer | **Missing** |
| `ruvector-mincut-wasm` | — | MinCut graph partitioning | **Missing** |
| `ruvector-nervous-system-wasm` | 0.1.0 | Nervous system architecture | **Missing** |
| `ruvector-sparse-inference-wasm` | — | Sparse inference engine | **Missing** |
| `ruvector-temporal-tensor-wasm` | — | Temporal tensor operations | **Missing** |
**18 WASM crates with no npm package.**
### 2.4 What Exists as Rust Crates Only (No WASM, No npm)
| Crate | Capability | Why It Matters |
|-------|-----------|---------------|
| `mcp-brain` | MCP stdio server for shared brain | Core brain MCP — only accessible via `cargo run` |
| `mcp-brain-server` | Cloud Run REST backend | Server-side only |
| `mcp-gate` | MCP coherence gate | Core MCP — only via `cargo run` |
| `cognitum-gate-kernel` | AI coherence gate kernel | Core reasoning engine |
| `cognitum-gate-tilezero` | TileZero game engine | Specialized |
| `prime-radiant` | Prime Radiant visualization | Specialized |
| `ruvector-delta-core` | Delta behavior tracking | Core capability, no JS access |
| `ruvector-delta-runtime` | Delta runtime | Runtime only |
| `ruvector-delta-serde` | Delta serialization | Utility |
| `ruvector-domain-expansion` | Transfer learning engine | Core brain capability |
| `ruvector-mincut` | SubpolynomialMinCut partitioning | Core graph capability |
| `ruvector-attention` | 46 attention mechanisms | Partially exposed via `@ruvector/attention` |
| `sona` | SONA learning engine | Partially exposed via `@ruvector/sona` |
| `rvf-federation` | Federated learning (PII strip, DP) | Core brain pipeline |
| `rvf-crypto` | Witness chains, Ed25519, SHAKE-256 | Core security |
| `agentic-robotics-*` (6 crates) | Autonomous robotics | Entire subsystem missing |
| `thermorust` | Thermal/energy modeling | Specialized |
| `ruvector-dither` | Dithering algorithms | Specialized |
| `ruvector-profiler` | Performance profiler | Dev tool (publish=false) |
**19+ crates with no JavaScript access at all.**
### 2.5 Fragmented CLI Entry Points
Users currently face 7+ separate CLI binaries:
| Binary | Package | Install |
|--------|---------|---------|
| `ruvector` | `ruvector` | `npx ruvector` |
| `pi-brain` / `π` | `@ruvector/pi-brain` | `npx pi-brain` |
| `ruvllm` | `@ruvector/ruvllm-cli` | `npx ruvllm` |
| `ruvbot` | `@ruvector/ruvbot` | `npx ruvbot` |
| `agentic-synth` | `@ruvector/agentic-synth` | `npx agentic-synth` |
| `rvf` | `@ruvector/rvf` | `npx rvf` |
| `postgres-cli` | `@ruvector/postgres-cli` | `npx @ruvector/postgres-cli` |
Each has its own install, auth, and configuration. There is no single `npx ruvector <anything>` that reaches them all.
## 3. Decision
Extend `npx ruvector` to be the **universal entry point** for the entire ecosystem. Every capability — whether it's a Rust crate, WASM binding, or npm package — should be reachable through `npx ruvector <group> <command>`. Missing npm packages for WASM crates should be published. Fragmented CLIs should be consolidated.
## 4. Proposed Command Hierarchy
```
npx ruvector
│
├── [EXISTING] Vector Database
│ ├── create Create vector database
│ ├── insert Insert vectors
│ ├── search ANN search with filters
│ ├── stats Database statistics
│ ├── benchmark Performance benchmarks
│ ├── info System info
│ ├── install Install optional packages
│ └── doctor Health check
│
├── [EXISTING] GNN
│ ├── gnn layer Create/test GNN layers
│ ├── gnn compress Adaptive tensor compression
│ ├── gnn search Differentiable search
│ └── gnn info Module info
│
├── [EXISTING] Attention
│ ├── attention compute 5 attention mechanisms
│ ├── attention benchmark Benchmark all types
│ ├── attention hyperbolic Hyperbolic geometry
│ ├── attention list List mechanisms
│ └── attention info Module details
│
├── [ADR-070] Brain (lazy: @ruvector/pi-brain)
│ ├── brain share Share knowledge
│ ├── brain search Semantic search
│ ├── brain get Retrieve by ID
│ ├── brain vote Quality vote
│ ├── brain list List memories
│ ├── brain delete Delete own
│ ├── brain transfer Domain transfer
│ ├── brain drift Drift detection
│ ├── brain partition Knowledge topology
│ ├── brain status System health
│ ├── brain sync LoRA weight sync
│ └── brain page Brainpedia CRUD
│
├── [ADR-070] Edge (lazy: @ruvector/edge-net)
│ ├── edge status Network status
│ ├── edge join Join as compute node
│ ├── edge balance rUv balance
│ ├── edge tasks Available compute tasks
│ └── edge dashboard Open dashboard
│
├── [ADR-070] MCP
│ ├── mcp start Start MCP server (stdio/SSE)
│ ├── mcp tools List available tools
│ └── mcp test Test connection
│
├── [ADR-070] Identity
│ ├── identity generate Generate π key
│ ├── identity show Display pseudonym
│ ├── identity export Encrypted backup
│ └── identity import Restore from backup
│
├── [NEW] LLM (lazy: @ruvector/ruvllm)
│ ├── llm chat Interactive chat
│ ├── llm embed Generate embeddings
│ ├── llm complete Text completion
│ ├── llm models List available models
│ ├── llm benchmark Inference benchmark
│ └── llm serve Start LLM server
│
├── [NEW] RVF (lazy: @ruvector/rvf)
│ ├── rvf read Read .rvf container
│ ├── rvf write Create .rvf container
│ ├── rvf validate Validate integrity
│ ├── rvf inspect Show segment layout
│ ├── rvf merge Merge containers
│ └── rvf convert Format conversions
│
├── [NEW] Graph (lazy: @ruvector/graph-wasm)
│ ├── graph create Create hypergraph
│ ├── graph query Cypher/SPARQL query
│ ├── graph import Import from CSV/JSON/Neo4j
│ ├── graph export Export to various formats
│ ├── graph visualize Text-based visualization
│ └── graph stats Graph statistics
│
├── [NEW] SONA (lazy: @ruvector/sona)
│ ├── sona train Train with trajectory
│ ├── sona patterns Search learned patterns
│ ├── sona optimize Run optimization
│ ├── sona export Export learned weights
│ └── sona stats Learning statistics
│
├── [NEW] Router (lazy: @ruvector/router)
│ ├── router classify Classify input to route
│ ├── router train Train router on examples
│ ├── router serve Start router server
│ └── router benchmark Route throughput test
│
├── [NEW] Quantum (lazy: @ruvector/ruqu-wasm)
│ ├── quantum sim Run quantum simulation
│ ├── quantum circuit Build quantum circuit
│ └── quantum stats Simulation statistics
│
├── [NEW] SNN (lazy: @ruvector/spiking-neural)
│ ├── snn train Train spiking network
│ ├── snn inference Run inference
│ └── snn benchmark SIMD performance test
│
├── [NEW] Delta (lazy: @ruvector/delta-wasm — TO PUBLISH)
│ ├── delta track Track behavior changes
│ ├── delta compare Compare two snapshots
│ └── delta report Drift report
│
├── [NEW] MinCut (lazy: @ruvector/mincut-wasm — TO PUBLISH)
│ ├── mincut partition Partition graph
│ ├── mincut certificate Verify cut certificate
│ └── mincut visualize Text visualization
│
├── [NEW] Synth (lazy: @ruvector/agentic-synth)
│ ├── synth generate Generate synthetic data
│ ├── synth validate Validate generated data
│ └── synth config Configure generators
│
├── [NEW] DNA (lazy: @ruvector/rvdna)
│ ├── dna analyze Analyze genomic data
│ ├── dna biomarker 20-SNP biomarker panel
│ └── dna report Generate report
│
├── [NEW] OCR (lazy: @ruvector/scipix)
│ ├── ocr extract Extract text from images
│ ├── ocr table Extract tables
│ └── ocr equations Extract equations
│
├── [NEW] DB (lazy: @ruvector/rvlite)
│ ├── db query SQL/SPARQL/Cypher query
│ ├── db import Import data
│ └── db export Export data
│
└── [NEW] Postgres (lazy: @ruvector/postgres-cli)
├── pg connect Connect to PostgreSQL
├── pg vector pgvector operations
└── pg migrate Schema migrations
```
**Total: ~100+ commands across 20+ groups** (up from 17 commands across 4 groups).
## 5. npm Packages to Publish
### 5.1 Priority 1 — WASM crates with brain/edge integration value
| WASM Crate | Proposed npm Package | Why |
|-----------|---------------------|-----|
| `ruvector-delta-wasm` | `@ruvector/delta-wasm` | Brain drift detection via `npx ruvector delta` |
| `ruvector-mincut-wasm` | `@ruvector/mincut-wasm` | Brain knowledge partitioning |
| `ruvector-domain-expansion-wasm` | `@ruvector/domain-expansion-wasm` | Brain transfer learning |
| `ruvector-economy-wasm` | `@ruvector/economy-wasm` | Edge-net economics (AMM, reputation) |
| `ruvector-learning-wasm` | `@ruvector/learning-wasm` | Online learning for edge nodes |
| `edge-net` (examples/) | `@ruvector/edge-net` | Edge-net WASM for `npx ruvector edge` |
### 5.2 Priority 2 — Advanced capabilities
| WASM Crate | Proposed npm Package | Why |
|-----------|---------------------|-----|
| `ruvector-attention-unified-wasm` | `@ruvector/attention-unified-wasm` | All 46 attention mechanisms |
| `ruvector-hyperbolic-hnsw-wasm` | `@ruvector/hyperbolic-hnsw-wasm` | Hyperbolic space search |
| `ruvector-nervous-system-wasm` | `@ruvector/nervous-system-wasm` | Full nervous system architecture |
| `ruvector-fpga-transformer-wasm` | `@ruvector/fpga-transformer-wasm` | FPGA-optimized inference |
| `ruvector-sparse-inference-wasm` | `@ruvector/sparse-inference-wasm` | Sparse model inference |
| `ruvector-graph-transformer-wasm` | `@ruvector/graph-transformer-wasm` | Graph transformers |
### 5.3 Priority 3 — Specialized
| WASM Crate | Proposed npm Package | Why |
|-----------|---------------------|-----|
| `ruvector-dag-wasm` | `@ruvector/dag-wasm` | DAG operations |
| `ruvector-math-wasm` | `@ruvector/math-wasm` | Math primitives |
| `ruvector-temporal-tensor-wasm` | `@ruvector/temporal-tensor-wasm` | Temporal operations |
| `ruvector-exotic-wasm` | `@ruvector/exotic-wasm` | Exotic neural architectures |
| `ruvector-mincut-gated-transformer` | `@ruvector/mincut-gated-wasm` | MinCut-gated attention |
## 6. Version Landscape
### 6.1 Mature (v2.x — stable API)
| Package | Version | Notes |
|---------|---------|-------|
| `@ruvector/ruvllm` | 2.5.1 | Self-learning LLM orchestration |
| `@ruvector/graph-node` | 2.0.2 | Native hypergraph (NAPI) |
| `@ruvector/graph-wasm` | 2.0.2 | WASM hypergraph |
| `@ruvector/ruqu-wasm` | 2.0.5 | Quantum simulations |
| `@ruvector/ruvllm-wasm` | 2.0.0 | WASM LLM |
| `micro-hnsw-wasm` | 2.3.2 | HNSW core |
### 6.2 Stable (v1.x — production-ready)
| Package | Version | Notes |
|---------|---------|-------|
| `@ruvector/agentic-integration` | 1.0.0 | Agent coordination |
| `@ruvector/burst-scaling` | 1.0.0 | Adaptive scaling |
| `@ruvector/spiking-neural` | 1.0.1 | SNN with SIMD |
| `@ruvector/ruvector-wasm-unified` | 1.0.0 | Unified WASM API |
### 6.3 Development (v0.x — breaking changes expected)
66 packages at v0.1.x-0.3.x, including the core `ruvector` CLI at v0.1.100.
### 6.4 Version Recommendation
The core `ruvector` package should target **v0.2.0** for the unified CLI expansion (ADR-070 commands), and **v1.0.0** when all Priority 1 packages are published and integrated.
## 7. Implementation Roadmap
### Phase 1: Consolidate Existing (2 weeks)
**Goal**: Bring existing npm packages into `npx ruvector` via lazy loading.
| Task | Effort | Packages |
|------|--------|----------|
| Add `brain` commands | 3 days | @ruvector/pi-brain |
| Add `llm` commands | 2 days | @ruvector/ruvllm |
| Add `rvf` commands | 2 days | @ruvector/rvf |
| Add `graph` commands | 2 days | @ruvector/graph-wasm |
| Add `sona` commands | 1 day | @ruvector/sona |
| Add `router` commands | 1 day | @ruvector/router |
| Add `quantum` commands | 1 day | @ruvector/ruqu-wasm |
| Add `snn` commands | 1 day | @ruvector/spiking-neural |
| Add `synth` commands | 1 day | @ruvector/agentic-synth |
| Add `db` commands | 1 day | @ruvector/rvlite |
| Add `pg` commands | 1 day | @ruvector/postgres-cli |
### Phase 2: Publish Missing WASM (3 weeks)
**Goal**: Build and publish Priority 1 WASM crates to npm.
| Task | Effort | Crate → Package |
|------|--------|-----------------|
| Build + publish delta-wasm | 2 days | ruvector-delta-wasm → @ruvector/delta-wasm |
| Build + publish mincut-wasm | 2 days | ruvector-mincut-wasm → @ruvector/mincut-wasm |
| Build + publish domain-expansion-wasm | 2 days | ruvector-domain-expansion-wasm → @ruvector/domain-expansion-wasm |
| Build + publish economy-wasm | 2 days | ruvector-economy-wasm → @ruvector/economy-wasm |
| Build + publish learning-wasm | 2 days | ruvector-learning-wasm → @ruvector/learning-wasm |
| Build + publish edge-net WASM | 3 days | edge-net → @ruvector/edge-net |
| Add `delta`, `mincut`, `edge` CLI groups | 3 days | CLI integration |
| Add `identity` commands | 2 days | Pi-Key management |
| Add `mcp` commands | 2 days | MCP server lifecycle |
### Phase 3: Publish Advanced WASM (2 weeks)
**Goal**: Build and publish Priority 2 WASM crates.
6 WASM packages to build with `wasm-pack` and publish.
### Phase 4: Polish and Release (1 week)
| Task | Effort |
|------|--------|
| `npx ruvector help` — comprehensive help with all groups | 1 day |
| `npx ruvector list` — list installed vs available packages | 1 day |
| `npx ruvector upgrade` — upgrade all @ruvector packages | 1 day |
| JSON output mode for all commands | 1 day |
| Pipe detection (auto-JSON when not TTY) | 0.5 day |
| Bump to v0.2.0, update README | 0.5 day |
## 8. Dependency Strategy
### 8.1 Bundled (always installed)
```
ruvector (core)
├── @ruvector/core (HNSW vector DB)
├── @ruvector/gnn (GNN layers)
├── @ruvector/attention (attention mechanisms)
├── @ruvector/sona (SONA learning)
├── commander, chalk, ora (CLI utilities)
└── @modelcontextprotocol/sdk (MCP protocol)
```
### 8.2 Optional Peer (lazy-loaded on first use)
```
@ruvector/pi-brain → brain commands
@ruvector/edge-net → edge commands
@ruvector/ruvllm → llm commands
@ruvector/rvf → rvf commands
@ruvector/graph-wasm → graph commands
@ruvector/ruqu-wasm → quantum commands
@ruvector/spiking-neural → snn commands
@ruvector/router → router commands
@ruvector/delta-wasm → delta commands
@ruvector/mincut-wasm → mincut commands
@ruvector/agentic-synth → synth commands
@ruvector/rvdna → dna commands
@ruvector/scipix → ocr commands
@ruvector/rvlite → db commands
@ruvector/postgres-cli → pg commands
```
### 8.3 Lazy Loading Pattern
```typescript
async function requirePackage(name: string): Promise<any> {
try {
return await import(name);
} catch {
console.error(chalk.red(`${name} is not installed.`));
console.error(chalk.yellow(` npm install ${name}`));
console.error(chalk.dim(` or: npx ruvector install ${name}`));
process.exit(1);
}
}
```
Each command group registers itself but defers the `import()` until the command is actually invoked. This keeps `npx ruvector` startup fast (~200ms) regardless of how many optional packages are installed.
## 9. Gap Summary
### By the Numbers
| Category | Available | In CLI | Gap |
|----------|-----------|--------|-----|
| npm packages | 79 | 4 bundled | **75 packages not in CLI** |
| WASM crates | 27 | 2 via npm | **18 without npm packages** |
| Rust-only crates | 19+ | 0 | **19+ with no JS access** |
| CLI entry points | 7 separate | 1 unified | **6 fragmented CLIs** |
| Commands | ~100 possible | 17 | **~83 missing commands** |
### Critical Gaps
1. **No brain access from CLI** — The shared intelligence at pi.ruv.io has no CLI path (ADR-070 proposed, not implemented)
2. **No edge network CLI** — Edge-net compute network unreachable from Node.js CLI
3. **No LLM commands** — ruvllm (v2.5.1, the most mature package) is a separate CLI
4. **No RVF commands** — The core file format has no CLI tooling
5. **No graph commands** — Hypergraph engine (v2.0.2) invisible to CLI users
6. **No identity management** — Pi-Key generation/management only in Rust
7. **18 WASM crates unpublished** — Significant WASM capabilities not accessible from JavaScript
8. **No unified discovery** — Users can't discover available capabilities from the CLI
## 10. Success Criteria
- [ ] `npx ruvector` lists all available command groups
- [ ] `npx ruvector brain search "auth"` works (ADR-070)
- [ ] `npx ruvector llm chat` works (ruvllm integration)
- [ ] `npx ruvector rvf inspect file.rvf` works
- [ ] `npx ruvector graph query "MATCH (n) RETURN n"` works
- [ ] `npx ruvector edge status` works (ADR-070)
- [ ] `npx ruvector identity generate` works (ADR-070)
- [ ] All 18 missing WASM crates published to npm
- [ ] `npx ruvector install` shows all optional packages with install status
- [ ] JSON output via `--json` flag on all commands
- [ ] Version bumped to 0.2.0 with full command hierarchy
## 11. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-065 | npm Publishing Strategy — tier-based publish order, semver, TypeScript requirements |
| ADR-070 | npx ruvector Unified Integration — brain, edge, mcp, identity commands (subset of this ADR) |
| ADR-069 | Edge-Net Integration — edge-net + brain distributed compute |
| ADR-059 | Shared Brain Google Cloud — backend that brain commands call |
| ADR-066 | SSE MCP Transport — MCP protocol for mcp commands |

View file

@ -0,0 +1,459 @@
# ADR-072: RVF Example Management and Downloads in npx ruvector
**Status**: Proposed
**Date**: 2026-02-28
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-070 (npx ruvector Unified Integration), ADR-044 (RVF Format Specification), ADR-065 (npm Publishing Strategy)
## 1. Context
The RuVector ecosystem currently ships 46 `.rvf` example files in `examples/rvf/output/` totaling ~11 MB. These demonstrate every RVF capability: basic vector stores, HNSW indexes, COW lineage chains, eBPF accelerators, TEE attestation, ZK witnesses, agent memory, MCP-in-RVF, self-booting kernels, and more.
Today these examples are accessed three ways, each with problems:
| Access Method | Problem |
|---|---|
| `npx ruvector rvf examples` (CLI) | Lists metadata only. `rvf download` fetches from GitHub raw — slow, no caching, no versioning, raw.githubusercontent.com has rate limits |
| `rvf_examples` MCP tool | Returns hardcoded list of 12 examples (out of 46). No download capability |
| Clone the repo | 800 MB+ clone for 11 MB of examples |
Additionally:
- The example catalog is hardcoded in two places (`cli.js` and `mcp-server.js`) and they're out of sync (CLI has 45, MCP has 12)
- No version pinning — examples may break with format changes
- No integrity verification — downloaded files aren't checksummed
- No offline cache — re-downloads every time
- New examples require a new npm publish to update the catalog
## 2. Decision
Host `.rvf` example files on Google Cloud Storage with a manifest-driven catalog. The CLI and MCP server read a versioned manifest from GCS to discover examples, download files with SHA-256 verification, and cache locally at `~/.ruvector/examples/`. A GitHub Actions workflow syncs examples to GCS on every push to `main`.
## 3. Architecture
### 3.1 GCS Bucket Layout
```
gs://ruvector-examples/
├── manifest.json ← current catalog (always latest)
├── v0.2.1/
│ ├── manifest.json ← pinned catalog for this version
│ ├── basic_store.rvf
│ ├── semantic_search.rvf
│ ├── rag_pipeline.rvf
│ ├── ... ← all 46+ examples
│ └── checksums.sha256 ← SHA-256 for every file
├── v0.2.0/
│ ├── manifest.json
│ └── ...
└── latest -> v0.2.1/ ← symlink-like redirect
```
**Public read access** via `allUsers:objectViewer` IAM. No authentication required for downloads.
**Cloud CDN** in front of the bucket for global edge caching. Typical latency: 20-50ms worldwide vs 200-500ms from raw.githubusercontent.com.
### 3.2 Manifest Format
```json
{
"version": "0.2.1",
"updated": "2026-02-28T12:00:00Z",
"base_url": "https://storage.googleapis.com/ruvector-examples/v0.2.1",
"total_size": "11.2 MB",
"examples": [
{
"name": "basic_store",
"file": "basic_store.rvf",
"size": 155648,
"size_human": "152 KB",
"sha256": "a1b2c3d4...",
"description": "1,000 vectors, dim 128, cosine metric",
"category": "core",
"tags": ["vectors", "cosine", "basic"],
"rvf_version": "1.0",
"segments": ["VEC", "META", "MANIFEST"],
"created": "2026-02-15"
}
],
"categories": {
"core": "Basic vector storage and search",
"ai": "AI agent, embedding, and RAG examples",
"security": "Attestation, ZK proofs, access control",
"compute": "eBPF, WASM, self-booting, kernels",
"lineage": "COW chains, derivation, reasoning",
"industry": "Finance, medical, legal domain examples",
"network": "Sync, handoff, distributed examples",
"integration": "MCP, PostgreSQL, serverless bridges"
}
}
```
### 3.3 Local Cache
```
~/.ruvector/
└── examples/
├── manifest.json ← cached manifest (TTL: 1 hour)
├── basic_store.rvf ← downloaded example
├── semantic_search.rvf
└── .cache-meta.json ← cache timestamps + checksums
```
Cache behavior:
- **Manifest TTL**: 1 hour. After that, fetch fresh manifest on next `examples` or `download` command
- **File cache**: Permanent until `rvf cache clear` or version change
- **Integrity**: SHA-256 verified on every download. Cached files re-verified on access if `--verify` flag used
- **Disk budget**: Default 100 MB. Oldest files evicted when budget exceeded. Configurable via `~/.ruvector/config.json`
### 3.4 CLI Commands
```
npx ruvector rvf examples # List all examples (from cached manifest)
npx ruvector rvf examples --category security # Filter by category
npx ruvector rvf examples --refresh # Force manifest refresh
npx ruvector rvf download basic_store # Download one example (cached)
npx ruvector rvf download --all # Download all examples (~11 MB)
npx ruvector rvf download --category ai # Download all AI examples
npx ruvector rvf download --verify # Re-verify cached files
npx ruvector rvf cache status # Show cache size, file count
npx ruvector rvf cache clear # Clear local cache
```
### 3.5 MCP Tool Updates
The `rvf_examples` MCP tool reads from the same manifest:
```json
{
"name": "rvf_examples",
"description": "List and download RVF example files from the ruvector catalog",
"inputSchema": {
"type": "object",
"properties": {
"filter": { "type": "string", "description": "Filter by name or description" },
"category": { "type": "string", "description": "Filter by category" },
"download": { "type": "string", "description": "Download a specific example by name" }
}
}
}
```
When `download` is specified, the tool downloads the file to the current working directory and returns the local path. This allows Claude Code to directly work with `.rvf` files.
### 3.6 Sync Pipeline (GitHub Actions)
```yaml
# .github/workflows/sync-rvf-examples.yml
name: Sync RVF Examples to GCS
on:
push:
branches: [main]
paths:
- 'examples/rvf/output/**'
- 'examples/rvf/generate_examples.rs'
jobs:
sync:
runs-on: ubuntu-latest
permissions:
id-token: write # Workload Identity Federation
steps:
- uses: actions/checkout@v4
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.GCS_SA }}
- name: Generate manifest
run: |
python3 scripts/generate-rvf-manifest.py \
--input examples/rvf/output/ \
--version $(jq -r .version npm/packages/ruvector/package.json) \
--output manifest.json
- name: Sync to GCS
uses: google-github-actions/upload-cloud-storage@v2
with:
path: examples/rvf/output/
destination: ruvector-examples/v${{ env.VERSION }}/
gzip: false # .rvf files are already compact
- name: Upload manifest
uses: google-github-actions/upload-cloud-storage@v2
with:
path: manifest.json
destination: ruvector-examples/v${{ env.VERSION }}/manifest.json
- name: Update latest manifest
run: |
gsutil cp gs://ruvector-examples/v${{ env.VERSION }}/manifest.json \
gs://ruvector-examples/manifest.json
```
### 3.7 Manifest Generator Script
```python
# scripts/generate-rvf-manifest.py
# Scans examples/rvf/output/, computes SHA-256, extracts RVF segment info,
# categorizes by naming convention, produces manifest.json
```
Categories are derived from the example name or explicit `category` field in a sidecar `.meta.json` file if present:
| Pattern | Category |
|---|---|
| `basic_store`, `semantic_search`, `filtered_search`, `quantization` | core |
| `agent_*`, `rag_*`, `embedding_*`, `ruvllm_*`, `ruvbot` | ai |
| `tee_*`, `zero_knowledge`, `access_control`, `sealed_engine` | security |
| `self_booting`, `ebpf_*`, `browser_wasm`, `linux_microkernel` | compute |
| `lineage_*`, `reasoning_*` | lineage |
| `financial_*`, `medical_*`, `legal_*` | industry |
| `network_*`, `agent_handoff_*` | network |
| `mcp_*`, `postgres_*`, `serverless`, `claude_code_*` | integration |
## 4. Fallback Strategy
If GCS is unreachable:
1. **Cached manifest**: Use `~/.ruvector/examples/manifest.json` if within TTL
2. **Stale manifest**: Use expired cached manifest with warning
3. **Hardcoded fallback**: Built-in minimal catalog (top 12 most popular examples) with GitHub raw URLs
4. **Offline mode**: `--offline` flag uses only cached files, no network
```
Download priority:
1. Local cache (~/.ruvector/examples/) ← 0ms, SHA-256 verified
2. GCS via Cloud CDN ← 20-50ms global
3. GCS direct ← 50-200ms
4. GitHub raw (fallback) ← 200-500ms, rate limited
```
## 5. Security
### 5.1 Integrity Verification
Every downloaded `.rvf` file is verified against the SHA-256 in the manifest before being cached or returned to the user. This prevents:
- **CDN poisoning**: Tampered files at the edge are detected
- **MITM attacks**: Even over HTTPS, defense-in-depth with content hashes
- **Cache corruption**: Local disk corruption caught on re-verify
### 5.2 Manifest Signing (Future)
Phase 2 will add Ed25519 signing of manifests:
```json
{
"version": "0.2.1",
"examples": [...],
"signature": "base64(Ed25519(manifest_without_signature))",
"signer": "ruvector-release-key-001"
}
```
The CLI will verify manifests against a pinned public key shipped in the npm package.
### 5.3 Path Safety
Download destination paths are validated to prevent directory traversal:
- `path.basename()` strips parent directory references
- Regex allows only `[a-zA-Z0-9_\-.]` characters in filenames
- Final path must resolve within the output directory
- These checks already exist in the current `rvf download` implementation
### 5.4 GCS Access Control
- Bucket: `allUsers:objectViewer` (public read)
- Write: Only the CI/CD service account via Workload Identity Federation
- No API keys or credentials shipped in the npm package
- Cloud Armor WAF rules for DDoS protection on CDN ingress
## 6. Implementation
### 6.1 File Changes
| File | Change |
|---|---|
| `npm/packages/ruvector/bin/cli.js` | Update `rvf examples` to fetch manifest from GCS. Update `rvf download` to use GCS URLs + SHA-256 verify + cache. Add `rvf cache` subcommand |
| `npm/packages/ruvector/bin/mcp-server.js` | Update `rvf_examples` handler to read manifest. Add download capability |
| `npm/packages/ruvector/src/rvf-catalog.ts` | Shared manifest fetcher, cache manager, integrity verifier |
| `scripts/generate-rvf-manifest.py` | Manifest generator from local examples |
| `.github/workflows/sync-rvf-examples.yml` | CI/CD sync pipeline |
### 6.2 Single Source of Truth
The `RVF_EXAMPLES` array currently hardcoded in `cli.js` (45 entries) and `mcp-server.js` (12 entries) is replaced by a shared manifest. Both read from:
```javascript
async function getRvfCatalog(opts = {}) {
const cacheDir = path.join(os.homedir(), '.ruvector', 'examples');
const manifestPath = path.join(cacheDir, 'manifest.json');
// Check cache
if (!opts.refresh && fs.existsSync(manifestPath)) {
const stat = fs.statSync(manifestPath);
const age = Date.now() - stat.mtimeMs;
if (age < 3600000) { // 1 hour TTL
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
}
}
// Fetch from GCS
const GCS_URL = 'https://storage.googleapis.com/ruvector-examples/manifest.json';
try {
const resp = await fetch(GCS_URL);
const manifest = await resp.json();
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
return manifest;
} catch {
// Fallback to cached (even if stale)
if (fs.existsSync(manifestPath)) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
}
// Final fallback: hardcoded minimal catalog
return BUILTIN_CATALOG;
}
}
```
## 7. GCS Setup
### 7.1 Bucket Creation
```bash
# Create bucket with Standard storage (frequently accessed)
gcloud storage buckets create gs://ruvector-examples \
--location=us-central1 \
--uniform-bucket-level-access \
--public-access-prevention=inherited
# Enable public read
gcloud storage buckets add-iam-policy-binding gs://ruvector-examples \
--member=allUsers \
--role=roles/storage.objectViewer
# Enable Cloud CDN (via load balancer backend bucket)
gcloud compute backend-buckets create ruvector-examples-backend \
--gcs-bucket-name=ruvector-examples \
--enable-cdn \
--cache-mode=CACHE_ALL_STATIC \
--default-ttl=3600
```
### 7.2 CI/CD Service Account
```bash
gcloud iam service-accounts create rvf-examples-sync \
--display-name="RVF Examples Sync"
gcloud storage buckets add-iam-policy-binding gs://ruvector-examples \
--member=serviceAccount:rvf-examples-sync@PROJECT.iam.gserviceaccount.com \
--role=roles/storage.objectAdmin
# Workload Identity Federation for GitHub Actions (no key file)
gcloud iam workload-identity-pools create github-pool \
--location=global
gcloud iam workload-identity-pools providers create-oidc github-provider \
--location=global \
--workload-identity-pool=github-pool \
--issuer-uri=https://token.actions.githubusercontent.com \
--attribute-mapping="google.subject=assertion.sub"
```
### 7.3 Cost Estimate
| Resource | Monthly Cost |
|---|---|
| GCS Standard (50 MB, 46 files x ~5 versions) | ~$0.01 |
| Cloud CDN egress (1 GB/month estimate) | ~$0.08 |
| Cloud CDN requests (10K/month estimate) | ~$0.01 |
| **Total** | **~$0.10/month** |
## 8. Example Categories and Counts
| Category | Count | Total Size | Description |
|---|---|---|---|
| core | 8 | ~5.1 MB | Basic stores, HNSW, quantization, filtering |
| ai | 8 | ~1.2 MB | Agent memory, RAG, embeddings, chatbot |
| security | 4 | ~439 KB | TEE, ZK, access control, sealed engine |
| compute | 4 | ~213 KB | eBPF, WASM, self-boot, microkernel |
| lineage | 5 | ~102 KB | COW chains, reasoning chains |
| industry | 3 | ~1.4 MB | Finance, medical, legal |
| network | 4 | ~146 KB | Sync, handoff, telemetry |
| integration | 6 | ~862 KB | MCP, PostgreSQL, serverless, Claude Code |
| **Total** | **46** | **~11.2 MB** | |
## 9. Migration Path
### Phase 1: GCS Setup + Sync (1 day)
- Create GCS bucket with public read
- Write `scripts/generate-rvf-manifest.py`
- Upload current 46 examples + manifest
- Create GitHub Actions workflow
### Phase 2: CLI Update (1 day)
- Replace hardcoded `RVF_EXAMPLES` with manifest fetcher
- Add SHA-256 integrity verification to downloads
- Add local cache at `~/.ruvector/examples/`
- Add `rvf cache status` and `rvf cache clear` subcommands
- Add `--category`, `--refresh`, `--offline`, `--verify` flags
### Phase 3: MCP Update (half day)
- Update `rvf_examples` handler to read from manifest
- Add `download` parameter to MCP tool
- Unify catalog between CLI and MCP (single code path)
### Phase 4: CDN + Monitoring (half day)
- Enable Cloud CDN via backend bucket
- Set up monitoring alerts for download errors
- Add analytics (download counts per example)
## 10. Testing
| Test | Description |
|---|---|
| Manifest fetch from GCS | Returns valid JSON with all 46 examples |
| Manifest cache TTL | Second fetch within 1 hour uses cache |
| Manifest refresh | `--refresh` bypasses cache |
| Download + SHA-256 verify | Downloaded file matches checksum |
| Cache hit | Second download of same file is instant |
| Offline mode | `--offline` uses only cached files |
| GCS unreachable | Falls back to cached manifest, then hardcoded catalog |
| Tampered file | SHA-256 mismatch detected and reported |
| Path traversal | `../../../etc/passwd.rvf` rejected |
| Category filter | `--category security` returns only 4 examples |
| Disk budget | Cache evicts oldest files when >100 MB |
| CI/CD sync | Push to `examples/rvf/output/` triggers GCS upload |
| Manifest generator | Produces valid manifest with correct checksums |
| MCP download | `rvf_examples` with `download` param returns local path |
## 11. Consequences
### Positive
- **Single source of truth**: Manifest replaces 2 hardcoded catalogs
- **Fast global downloads**: Cloud CDN edge caching (20-50ms vs 200-500ms)
- **Integrity**: SHA-256 verification on every download
- **Offline support**: Local cache + `--offline` flag
- **Auto-sync**: New examples available without npm republish
- **Cheap**: ~$0.10/month for storage + CDN
- **Version pinning**: Each ruvector version maps to a pinned manifest
### Negative
- **GCS dependency**: New infrastructure to maintain (mitigated by GitHub raw fallback)
- **Cache management**: Users accumulate cached files (mitigated by disk budget + clear command)
- **Manifest staleness**: 1-hour TTL means new examples take up to 1 hour to appear
### Neutral
- Existing `rvf download` command retains same UX, just faster and verified
- `rvf_examples` MCP tool gains download capability
- GitHub raw URLs continue to work as fallback
## 12. Related ADRs
| ADR | Relationship |
|---|---|
| ADR-044 | RVF Format Specification — defines the `.rvf` binary format these examples demonstrate |
| ADR-059 | Shared Brain Google Cloud — same GCP project, similar GCS patterns |
| ADR-065 | npm Publishing Strategy — example catalog decoupled from npm publish cycle |
| ADR-070 | npx ruvector Unified Integration — `rvf` command group that hosts these commands |

View file

@ -0,0 +1,284 @@
# ADR-073: π.ruv.io Platform Security Audit & Optimization
**Status**: Accepted
**Date**: 2026-03-01
**Authors**: RuVector Team
**Deciders**: ruv
**Related**: ADR-070 (npx ruvector Unified Integration), ADR-064 (Pi Brain Infrastructure), ADR-066 (SSE MCP Transport), ADR-058 (Hash Security Optimization)
## 1. Context
A comprehensive deep review of the `ruvector` npm package (v0.2.2) — the unified CLI, MCP server, and SDK for the π.ruv.io collective intelligence platform — was performed. The review tested all 48 CLI commands across 12 command groups and 91 MCP tools using a live PI key against the production π.ruv.io endpoint.
The audit revealed security vulnerabilities, error handling gaps, and UX friction points that needed immediate remediation before the v0.2.3 release.
### Test Environment
| Component | Value |
|-----------|-------|
| Package | `ruvector@0.2.2` |
| Node.js | ≥18.0.0 |
| CLI Commands | 48 across 12 groups |
| MCP Tools | 91 across 12 categories |
| Endpoint | `https://pi.ruv.io` |
| Transport | stdio + SSE dual-mode |
### Test Results Summary
| Metric | Value |
|--------|-------|
| Unit tests | 55/55 pass |
| CLI commands tested | 36 |
| Commands passing | 27/36 (75%) |
| CLI startup time | 54.6ms avg |
| MCP server init | 246ms |
## 2. Decision
Fix all HIGH and MEDIUM severity issues found during the audit, publish v0.2.3 with security patches, and document the findings for future reference.
## 3. Findings & Remediations
### 3.1 HIGH: PI Key Exposed in URL Path (FIXED)
**Issue**: The `edge balance` command placed the raw PI key directly in the URL path:
```javascript
// BEFORE (vulnerable)
const resp = await fetch(`${EDGE_GENESIS}/balance/${piKey}`, ...);
```
This is a critical security violation — URL paths are logged by proxies, CDNs, web servers, and browser history. A 64-character hex PI key in the path leaks the user's identity and authentication credential.
**Fix**: Derive a SHAKE-256 pseudonym from the PI key and use that in the URL. The raw key is sent only in the `Authorization` header (which is not logged by standard infrastructure):
```javascript
// AFTER (secure)
const pseudonym = require('crypto')
.createHash('shake256', { outputLength: 16 })
.update(piKey)
.digest('hex');
const resp = await fetch(`${EDGE_GENESIS}/balance/${pseudonym}`, {
headers: { 'Authorization': `Bearer ${piKey}` }
});
```
**Files modified**: `bin/cli.js`, `bin/mcp-server.js`
**Rationale**: SHAKE-256 is the same hash function used throughout the RVF wire format (ADR-058). The 16-byte (32-hex-char) pseudonym is sufficient for routing without revealing the key. The Authorization header is the standard HTTP mechanism for bearer tokens and is stripped by well-configured reverse proxies before logging.
### 3.2 HIGH: SONA Status Native Binding Crash (NOTED)
**Issue**: `npx ruvector sona status` crashes with `Module not found` when `@ruvector/sona` native bindings aren't installed. The SONA package has optional native acceleration via N-API that requires platform-specific compilation.
**Status**: Not fixed in this release — the crash is in the `@ruvector/sona` package itself, not in the `ruvector` CLI. The CLI already lazy-loads SONA and catches import errors for the top-level module, but internal native binding failures propagate.
**Mitigation**: Future `@ruvector/sona` release should wrap native binding calls in try-catch with WASM fallback.
### 3.3 MEDIUM: Edge Genesis 404 Crash (FIXED)
**Issue**: Edge network commands (`edge genesis`, `edge balance`) crashed when the backend returned non-JSON error responses (404, 502, etc.):
```
SyntaxError: Unexpected token 'N', "Not Found" is not valid JSON
```
The code called `resp.json()` without checking `resp.ok` first.
**Fix**: Added response status check before JSON parsing in both CLI and MCP server:
```javascript
if (!resp.ok) {
const errText = await resp.text().catch(() => resp.statusText);
console.error(chalk.red(`Edge network returned ${resp.status} ${resp.statusText}`));
process.exit(1);
}
```
**Files modified**: `bin/cli.js`, `bin/mcp-server.js`
### 3.4 MEDIUM: `hooks remember` Required `-t` Flag (FIXED)
**Issue**: `npx ruvector hooks remember -v "some value"` failed with `error: required option '-t, --type <type>' not specified`. For a convenience command, requiring the type flag on every invocation is excessive UX friction.
**Fix**: Changed from `requiredOption` to `option` with a default value of `'general'`:
```javascript
// BEFORE
.requiredOption('-t, --type <type>', 'Memory type')
// AFTER
.option('-t, --type <type>', 'Memory type', 'general')
```
**Files modified**: `bin/cli.js`
### 3.5 MEDIUM: `@ruvector/pi-brain` Not Installed (NOTED)
**Issue**: Brain commands fail when `@ruvector/pi-brain` is not installed. It's declared as an optional `peerDependency` in `package.json`, but the error message isn't user-friendly.
**Status**: Working as designed — pi-brain is an optional peer dependency. The lazy-load pattern already provides a clear error: `"Install @ruvector/pi-brain for brain commands"`. Users who want brain features install it separately.
### 3.6 LOW: CLI Monolith Size (NOTED)
**Issue**: `bin/cli.js` is 8,582 lines — a large single file. While it works correctly and loads fast (54.6ms), it makes maintenance harder.
**Status**: Deferred. The file loads fast due to lazy-loading of heavy dependencies (GNN, attention, ora). Splitting would add complexity without performance benefit. If the file exceeds 10,000 lines, consider splitting by command group.
### 3.7 LOW: `hooks recall` Non-Semantic Search (NOTED)
**Issue**: `hooks recall --query` uses exact string matching (`includes()`) rather than semantic/fuzzy search. This limits discovery of related memories.
**Status**: Deferred. Semantic search would require loading SONA embeddings for every recall, adding ~200ms latency. The current approach is fast and predictable.
## 4. Performance Benchmarks
### CLI Startup
| Run | Time (ms) |
|-----|-----------|
| 1 | 56 |
| 2 | 54 |
| 3 | 53 |
| 4 | 55 |
| 5 | 55 |
| **Avg** | **54.6** |
Fast startup is achieved through lazy-loading: heavy modules (GNN, attention, SONA, ora) are only `require()`'d when their commands are invoked.
### MCP Server
| Metric | Value |
|--------|-------|
| Init time | 246ms |
| Tools registered | 91 |
| SSE health response | <10ms |
| stdio JSON-RPC | Content-Length framed |
### Command Groups (12)
| Group | Commands | Status |
|-------|----------|--------|
| core | 7 | All pass |
| brain | 6 | Pass (requires @ruvector/pi-brain) |
| edge | 5 | Pass (with 404 fix) |
| identity | 3 | All pass |
| sona | 4 | 3/4 (native binding issue) |
| hooks | 4 | All pass (with default fix) |
| mcp | 3 | All pass |
| gnn | 4 | All pass |
| attention | 3 | All pass |
| rvf | 3 | All pass |
| solver | 3 | All pass |
| parallel | 3 | All pass |
## 5. Security Architecture
### PI Key Derivation Chain
A single 64-hex PI key derives all identity components:
```
PI Key (64 hex chars)
├── SHAKE-256(key, 16 bytes) → Pseudonym (32 hex chars)
│ └── Used in: URL paths, public identifiers
├── HMAC-SHA256(key, "mcp-auth") → MCP Token
│ └── Used in: MCP server authentication
└── SHA-512(key) → first 32 bytes → Ed25519 Seed
└── Used in: Edge network identity, signing
```
### Security Principles Applied
1. **Never expose keys in URLs** — use derived pseudonyms (SHAKE-256)
2. **Constant-time comparison** — `subtle::ConstantTimeEq` for wire hash verification
3. **Lazy credential loading** — PI key read from env/file only when needed
4. **AES-256-GCM** for key export with password-derived encryption
5. **No credential logging** — CLAUDE.md explicitly forbids echoing/printing credentials
## 6. Server API Deep Review (27 Endpoints)
A comprehensive validation of all 27 REST endpoints on the live π.ruv.io backend was performed.
### Endpoint Test Results
| # | Endpoint | Method | Status | Verdict |
|---|----------|--------|--------|---------|
| 1 | `/v1/health` | GET | 200 | Pass — returns uptime, version, persistence mode |
| 2 | `/v1/challenge` | GET | 200 | Pass — issues UUID nonce with 5-min TTL |
| 3 | `/v1/memories` | POST | 201 | Pass — creates memory with Firestore write-through |
| 4 | `/v1/memories/search` | GET | 200 | Pass — SHAKE-256 hash-based embedding + attention ranking |
| 5 | `/v1/memories/list` | GET | 200 | Pass — paginated listing |
| 6 | `/v1/memories/{id}` | GET | 200 | Pass — full BrainMemory with provenance |
| 7 | `/v1/memories/{id}/vote` | POST | 200/403 | Pass — self-vote blocked, Bayesian BetaParams updated |
| 8 | `/v1/memories/{id}` | DELETE | 204 | Pass — contributor-scoped deletion |
| 9 | `/v1/transfer` | POST | 200 | Pass — returns acceleration_factor, transfer_success |
| 10 | `/v1/drift` | GET | 200 | Pass — VectorDelta CV computation |
| 11 | `/v1/partition` | GET | 200 | Pass — MinCut clusters (data-dependent) |
| 12 | `/v1/status` | GET | 200 | Pass — comprehensive stats |
| 13 | `/v1/lora/latest` | GET | 200 | Pass — consensus weights (null until 3 submissions) |
| 14 | `/v1/lora/submit` | POST | 200 | Pass — validates shape (rank=2, hidden_dim=128) |
| 15 | `/v1/training/preferences` | GET | 200 | Pass — vote preference pairs for RLHF |
| 16 | `/v1/pages` | POST | 201/403 | Pass — reputation-gated page creation |
| 17 | `/v1/pages/{id}` | GET | 200/404 | Pass |
| 18 | `/v1/pages/{id}/deltas` | POST | 200/400 | Pass — validates evidence link requirements |
| 19 | `/v1/pages/{id}/deltas` | GET | 200 | Pass |
| 20 | `/v1/pages/{id}/evidence` | POST | 200/400 | Pass — validates evidence type enum |
| 21 | `/v1/pages/{id}/promote` | POST | 200/403 | Pass — quality threshold gate |
| 22 | `/v1/nodes` | GET | 200 | Pass — lists non-revoked WASM nodes |
| 23 | `/v1/nodes` | POST | 201/403 | Pass — reputation-gated node publishing |
| 24 | `/v1/nodes/{id}` | GET | 200/404/410 | Pass — 410 Gone for revoked nodes |
| 25 | `/v1/nodes/{id}/wasm` | GET | 200/404 | Pass — binary download with immutable cache |
| 26 | `/v1/nodes/{id}/revoke` | POST | 200/404 | Pass — contributor-scoped revocation |
| 27 | `/` + `/origin` + `/sse` | GET | 200 | Pass — landing page, origin story, SSE transport |
### Issues Found & Fixed
| Severity | Issue | Fix |
|----------|-------|-----|
| **HIGH** | Reputation never updated on share/vote — `ReputationManager` wired for reads only | Added `record_contribution()` on share, `update_reputation_from_vote()` on vote, `check_poisoning()` on downvotes |
| **HIGH** | `contribution_count` never incremented | Now incremented in `record_contribution()` with Firestore write-through |
| **MEDIUM** | Seed contributor stuck at cold-start 0.1 composite | Fixed — contributions now build reputation via EMA uptime + accuracy updates |
| **MEDIUM** | LoRA submit schema undocumented | Requires `LoraSubmission { down_proj, up_proj, rank, hidden_dim, evidence_count }` with rank=2, hidden_dim=128 |
| **LOW** | Evidence type `code_reference` in docs doesn't exist | Only supports: `test_pass`, `build_success`, `metric_improval`, `peer_review` |
### Feature Implementation Status
| Feature | Status | Notes |
|---------|--------|-------|
| Memory CRUD | Fully implemented | Firestore write-through, graph sync, drift recording |
| Voting + Quality | Fully implemented | Bayesian BetaParams, self-vote block, duplicate block, preference pairs |
| Reputation System | **Fixed** — now fully wired | EMA accuracy, uptime decay, poisoning penalty, contribution counting |
| Transfer Learning | Fully implemented | DomainExpansionEngine with acceleration factor |
| Drift Monitoring | Fully implemented | VectorDelta with CV threshold detection |
| MinCut Partitioning | Fully implemented | SubpolynomialMinCut with CognitiveEngine |
| LoRA Federation | Fully implemented | Gate B: per-parameter median + MAD outlier filtering + reputation-weighted trimmed mean |
| Brainpedia Pages | Fully implemented | Reputation-gated creation, delta submissions, evidence, promotion |
| WASM Nodes | Fully implemented | Reputation-gated publishing, SHA-256 verification, revocation |
| SSE MCP Transport | Fully implemented | JSON-RPC over SSE with session management |
| Challenge Nonce | Fully implemented | Replay protection with 5-min TTL |
| Rate Limiting | Fully implemented | Per-contributor token bucket (100 writes/hr, 1000 reads/hr) |
## 7. Version History
| Version | Date | Changes |
|---------|------|---------|
| 0.2.1 | 2026-02-28 | Initial 48-command CLI, 91 MCP tools |
| 0.2.2 | 2026-02-28 | Core version display fix, chalk ESM compat |
| 0.2.3 | 2026-03-01 | Security: PI key URL fix, edge 404 handling, hooks UX |
## 7. Consequences
### Positive
- PI key no longer leaked in URL paths across all edge commands
- Edge commands gracefully handle backend errors instead of crashing
- `hooks remember` is more ergonomic with sensible defaults
- Comprehensive benchmark baseline established for future optimization
- All 55 unit tests continue to pass
### Negative
- SONA native binding crash remains (fix requires @ruvector/sona release)
- CLI monolith continues to grow (8,582 lines)
- hooks recall still uses exact match (semantic search deferred)
### Risks
- SHAKE-256 pseudonym collision: negligible at 128 bits (2^64 birthday bound)
- Edge 404 text fallback: `resp.text()` could theoretically be large; mitigated by Cloud Run response limits

View file

@ -0,0 +1,167 @@
# ADR-074: RuvLLM Neural Embedding Integration
**Status:** Implemented (Phase 2 — RlmEmbedder Active)
**Date:** 2026-03-01 (Phase 1), 2026-03-03 (Phase 2)
**Author:** ml-engineer, platform-eng
## Context
The π.ruv.io shared brain server previously relied on client-side embedding generation (SHA-256 hash or token-averaged hashes) which produced poor-quality embeddings that failed cosine similarity search. A keyword search fallback was added as a stopgap, but vector-native search is essential for scaling beyond trivial corpus sizes.
The ruvllm crate provides a pure-Rust embedding pipeline with three tiers:
1. **HashEmbedder** — FNV-1a hash with character bigrams, L2-normalized (no model required)
2. **RlmEmbedder** — Recursive context-aware embeddings conditioned on a neighbor corpus
3. **Candle sentence transformer** — Neural sentence embeddings (all-MiniLM-L6-v2 or similar)
## Decision
Integrate ruvllm into mcp-brain-server with a phased approach:
### Phase 1 (Implemented): HashEmbedder
- Add `ruvllm = { path = "../ruvllm", default-features = false, features = ["minimal"] }` dependency
- Create `src/embeddings.rs` wrapping `ruvllm::bitnet::rlm_embedder::HashEmbedder`
- Server auto-generates 128-dim L2-normalized embeddings when clients send empty `embedding: []`
- Both storage and search use the same embedding dimension
- No model download, no cold-start penalty, deterministic output
### Phase 2 (Implemented 2026-03-03): RlmEmbedder
- `FlatNeighborStore` populated from all stored memories on startup
- `RlmEmbedder<HashEmbedder, FlatNeighborStore>` active at **50+ corpus documents** (was 1000)
- Storage uses **CorpusConditioned** variant (base=0.7, context=0.25, anti=0.05)
- Search uses **QueryConditioned** variant (base=0.6, context=0.3, anti=0.1)
- **Re-embedding on startup**: When RLM activates, all persisted memories are re-embedded with CorpusConditioned RLM for embedding space consistency (stored embeddings may have been HashEmbedder-generated)
- Graph similarity threshold raised from 0.30 → 0.55 for RLM (contextual gravity makes embeddings more similar)
- Clone derives added upstream to `HashEmbedder` and `FlatNeighborStore`
### Phase 3 (Future): Candle Sentence Transformer
- Enable `candle` feature for ruvllm
- Load all-MiniLM-L6-v2 (~90MB) or gte-small (~30MB) model
- 384-dim sentence embeddings with true semantic understanding
- Trade-off: model download time vs. embedding quality
- Mitigate cold-start with model pre-loading in Cloud Run min-instances
## Architecture
```
Client Request (empty embedding)
│
▼
┌──────────────────────────┐
│ routes.rs: share_memory │
│ ┌────────────────────┐ │
│ │ Auto-embed check: │ │
│ │ empty or dim≠128? │──── Yes ──▶ EmbeddingEngine::embed_for_storage()
│ │ │ │ │
│ └────────────────────┘ │ ▼
│ │ No │ ruvllm::HashEmbedder::embed()
│ ▼ │ FNV-1a + char bigrams + L2 norm
│ Use client embedding │ │
│ │ │ ▼
│ ▼ │ 128-dim Vec<f32>
│ Verifier::verify_share │◀─────────────┘
│ (on final embedding) │
└──────────────────────────┘
Client Search (text query)
│
▼
┌──────────────────────────┐
│ routes.rs: search_memories│
│ ┌────────────────────┐ │
│ │ Has text query q? │──── Yes ──▶ EmbeddingEngine::embed()
│ │ │ │ │
│ └────────────────────┘ │ ▼
│ │ No │ Same HashEmbedder pipeline
│ ▼ │ │
│ Return empty │ ▼
│ │ cosine_similarity(query_emb, stored_emb)
│ │ → reputation-weighted ranking
└──────────────────────────┘
```
## Key Design Decisions
1. **Server-side embedding**: Clients send empty `embedding: []` and the server generates. This ensures:
- Consistent dimension (128) across all memories
- No client-side embedding logic needed
- Future backend upgrades transparent to clients
- Backward compatible: clients can still send pre-computed embeddings
2. **`minimal` feature**: Avoids pulling in candle-core (~50 crates). HashEmbedder is pure Rust with zero external dependencies.
3. **128-dim**: Matches existing SONA engine, cognitive engine, and ranking engine dimensions. Lower than typical sentence transformer (384) but sufficient for hash-based embeddings.
4. **Embedding verification after auto-generation**: The share handler generates embeddings before calling `Verifier::verify_share()`, so the verification validates the server-generated embedding (not an empty array).
5. **Corpus tracking**: `EmbeddingEngine::add_to_corpus()` tracks corpus size for future RlmEmbedder integration. Status endpoint reports `embedding_corpus` count.
## Dependencies Added
```toml
ruvllm = { path = "../ruvllm", default-features = false, features = ["minimal"] }
```
Transitive: `ruvector-core`, `ruvector-sona` (already in tree)
## Files Changed
| File | Change |
|------|--------|
| `crates/mcp-brain-server/Cargo.toml` | Added ruvllm dependency |
| `crates/mcp-brain-server/src/embeddings.rs` | New: EmbeddingEngine wrapping HashEmbedder |
| `crates/mcp-brain-server/src/lib.rs` | Added `pub mod embeddings` |
| `crates/mcp-brain-server/src/types.rs` | Added `embedding_engine` to AppState and StatusResponse |
| `crates/mcp-brain-server/src/routes.rs` | Auto-embed in share, embed-based search, status fields |
## Consequences
### Positive
- Vector similarity search works with consistent 128-dim embeddings
- No model download or external service required
- Deterministic: same text always produces same embedding
- Zero cold-start penalty (HashEmbedder is <1ms)
- Clients simplified: no embedding logic needed
### Negative
- RLM contextual gravity reduces discriminative power on homogeneous corpora — keyword matching must remain dominant signal
- 128-dim is lower fidelity than 384-dim sentence transformers
- Re-embedding on startup adds ~2-3s to cold start with 237 memories
- FNV-1a hash collisions possible for very similar token patterns (base embedder)
### Neutral
- Keyword search still primary ranking signal (keyword floor +1.0 always outranks embedding-only)
- Future upgrade to candle sentence transformer is backward-compatible (same dimension)
## Metrics (Phase 1 Deployment — 2026-03-01)
- **Seeded:** 37 memories, 19 contributors
- **Search hit rate:** 10/10 queries return results
- **Graph:** 37 nodes, 200 edges
- **Clusters:** 7 (category-based partition)
- **Avg quality:** 0.838
- **Embedding corpus:** 37 entries
- **Build size:** No significant increase (ruvllm minimal is pure Rust)
## Metrics (Phase 2 Deployment — 2026-03-03)
- **Memories:** 237, **Contributors:** 17
- **Embedding engine:** `ruvllm::RlmEmbedder` (context-aware, activated at 50+ docs)
- **Search P@1:** 100% (30/30 benchmark queries)
- **Search P@3:** 100% (30/30)
- **Graph:** 237 nodes, 827 edges (threshold 0.55)
- **Clusters:** 20 (meaningful MinCut partitions)
- **Avg quality:** 0.73
- **Votes:** 608
- **LoRA epoch:** 2
### Search Intelligence Stack (Phase 2)
| Layer | Signal | Weight (keyword path) | Weight (no keyword) |
|-------|--------|----------------------|---------------------|
| Keyword matching | Word-boundary title/tag/category/content | 0.85 × boost + 1.0 floor | — |
| RLM embedding similarity | QueryConditioned cosine | 0.05 | 0.45 |
| Graph PPR (ForwardPushSolver) | PageRank over knowledge graph | 0.04 | 0.25 |
| Vote quality (Bayesian Beta) | Learning-to-rank from 608 votes | 0.03 | 0.15 |
| Reputation | Multi-factor contributor trust | 0.03 | 0.15 |
| Query expansion | 32 synonym rules (abbreviations) | implicit | implicit |
| Attention ranking | TopologyGatedAttention post-processing | post-score | post-score |

View file

@ -0,0 +1,354 @@
# ADR-075: Wire Full RVF AGI Stack into mcp-brain-server
**Status**: Implemented
**Date**: 2026-03-03
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-058 (Hash Security Optimization), ADR-059 (Shared Brain Google Cloud), ADR-060 (Shared Brain Capabilities), ADR-029 (RVF Canonical Format), ADR-057 (Federated RVF Transfer Learning)
## 1. Context
The Shared Brain server (`crates/mcp-brain-server/`) is deployed and operational at π.ruv.io with 237+ memories, P@1 100%, and 20 knowledge clusters. However, it currently uses **inline reimplementations** of cryptographic operations (sha2/sha3/ed25519-dalek) instead of the production RVF crates. Specifically:
- `verify.rs` has dead-code functions (`verify_ed25519_signature`, `verify_witness_chain`, `verify_content_hash`) that are never called from route handlers
- PII checking uses 8 simple string patterns instead of the 12-rule regex `PiiStripper` from `rvf-federation`
- No differential privacy noise injection on embeddings
- No witness chains linking memory operations
- No RVF container construction pipeline
- No `NegativeCache` or `BudgetTokenBucket` from `rvf-runtime`
The plan described in ADR-060 Section 2.5 specifies a canonical 10-segment RVF container layout per memory. This ADR documents the decision to wire in the real RVF crate implementations to activate all planned security and AGI features.
## 2. Decision
Replace inline crypto and security implementations in `mcp-brain-server` with the production RVF crate stack: `rvf-crypto`, `rvf-wire`, `rvf-types`, `rvf-federation`, and `rvf-runtime`.
### 2.1 New Dependencies
Add to `crates/mcp-brain-server/Cargo.toml`:
```toml
# RVF AGI Format Stack
rvf-types = { path = "../rvf/rvf-types", features = ["std"] }
rvf-crypto = { path = "../rvf/rvf-crypto" }
rvf-wire = { path = "../rvf/rvf-wire" }
rvf-federation = { path = "../rvf/rvf-federation", features = ["serde"] }
rvf-runtime = { path = "../rvf/rvf-runtime" }
```
### 2.2 Phase 1: Replace Inline Crypto with rvf-crypto
- `verify_content_hash()`: Delegate to `rvf_crypto::shake256_256()` + constant-time compare
- `verify_witness_chain()`: Keep old string-step method for backward compat; add `verify_rvf_witness_chain()` using `rvf_crypto::verify_witness_chain(data: &[u8]) -> Result<Vec<WitnessEntry>>`
- `verify_ed25519_signature()`: Keep as-is (already uses ed25519-dalek; Cargo dedupes)
- New: `verify_rvf_segment_signature()` using `rvf_crypto::verify_segment(header, payload, footer, pubkey) -> bool`
### 2.3 Phase 2: PII Stripping with rvf-federation PiiStripper
Replace the 8-pattern inline PII check with `rvf_federation::PiiStripper`:
- 12 regex rules: Unix/Windows paths, IPv4/IPv6, emails, API keys (sk-, AKIA, ghp_), Bearer tokens, env vars, @usernames
- `strip_fields(&[(&str, &str)]) -> (Vec<(String, String)>, RedactionLog)` returns redacted fields + attestation log
- `contains_pii(input: &str) -> bool` for backward-compatible rejection check
- `RedactionLog` stored as JSON on `BrainMemory.redaction_log`
PII stripping occurs **after** `verify_share()` and **before** storage — redacted values replace raw input.
### 2.4 Phase 3: Differential Privacy for Embeddings
Wire `rvf_federation::DiffPrivacyEngine::gaussian(epsilon, delta, sensitivity, clipping_norm)`:
- After embedding generation, convert `Vec<f32>` → `Vec<f64>`, call `dp.add_noise(&mut params) -> DiffPrivacyProof`, convert back
- Store proof JSON on `BrainMemory.dp_proof`
- Feature-gated by `RVF_DP_ENABLED` (default: `false`) — enable only after P@1 regression testing
- Default epsilon=1.0, configurable via `RVF_DP_EPSILON`
**Risk**: DP noise reduces embedding precision. Start with epsilon=1.0 (low noise). Benchmark P@1 before enabling in production.
### 2.5 Phase 4: Witness Chains for Memory Operations
Build a 3-entry linked witness chain per memory using `rvf_crypto::create_witness_chain()`:
| Entry | Type | Action Hash |
|-------|------|-------------|
| 1 | PROVENANCE (0x01) | SHAKE-256 of PII-stripped content |
| 2 | COMPUTATION (0x02) | SHAKE-256 of embedding bytes |
| 3 | PROVENANCE (0x01) | SHAKE-256 of final memory JSON |
Each entry: `prev_hash` linked by `create_witness_chain`, `action_hash` = `shake256_256(data)`, `timestamp_ns` = current time, `witness_type` = entry type.
Chain bytes stored on `BrainMemory.witness_chain`. `witness_hash = hex(shake256_256(chain_bytes))`.
Adversarial detection: `rvf_runtime::is_degenerate_distribution(distances, n_probe)` — log-only, no rejection, to avoid false positives.
### 2.6 Phase 5: RVF Container Construction Pipeline
New file `crates/mcp-brain-server/src/pipeline.rs` (~120 lines):
Assemble segments using `rvf_wire::write_segment(seg_type, payload, flags, segment_id)`:
| Segment | Type | Content |
|---------|------|---------|
| VEC (0x01) | Embedding | f32 LE bytes |
| META (0x07) | Metadata | JSON (title, content, tags, category) |
| WITNESS (0x0A) | Audit trail | Witness chain bytes |
| DiffPrivacyProof (0x34) | Privacy attestation | Proof JSON bytes (if DP enabled) |
| RedactionLog (0x35) | PII attestation | Redaction JSON bytes (if PII stripped) |
Container uploaded to GCS. Segment count reported in `ShareResponse.rvf_segments`.
### 2.7 Phase 6: Enhanced Rate Limiting + Negative Cache
Wire `rvf_runtime::NegativeCache::new(threshold: 5, window: 3600s, max_entries: 10_000)`:
- `QuerySignature::from_query(&[f32])` — FNV-1a of int8-quantized vector
- Check negative cache before full search in `search_memories()`
- Record degenerate embeddings in negative cache after adversarial detection
## 3. Type Changes
### BrainMemory (new fields, all `Option<T>` with `#[serde(default)]`)
| Field | Type | Phase | Purpose |
|-------|------|-------|---------|
| `redaction_log` | `Option<String>` | 2 | JSON-serialized `RedactionLog` |
| `dp_proof` | `Option<String>` | 3 | JSON-serialized `DiffPrivacyProof` |
| `witness_chain` | `Option<Vec<u8>>` | 4 | Raw witness chain bytes |
### AppState (new fields)
| Field | Type | Phase | Purpose |
|-------|------|-------|---------|
| `dp_engine` | `Arc<Mutex<DiffPrivacyEngine>>` | 3 | Shared DP noise generator |
| `negative_cache` | `Arc<Mutex<NegativeCache>>` | 6 | Degenerate query cache |
### ShareResponse (new fields)
| Field | Type | Phase | Purpose |
|-------|------|-------|---------|
| `witness_hash` | `String` | 4 | Hex SHAKE-256 of witness chain |
| `rvf_segments` | `Option<u32>` | 5 | Segment count in RVF container |
### StatusResponse (new fields)
| Field | Type | Phase | Purpose |
|-------|------|-------|---------|
| `dp_epsilon` | `f64` | 3 | Current DP epsilon parameter |
| `dp_budget_used` | `f64` | 3 | Fraction of privacy budget consumed |
| `rvf_segments_per_memory` | `f64` | 6 | Average segments per RVF container |
## 4. Feature Gating
All new features are controlled by environment variables for gradual rollout:
| Feature | Env Var | Default | Risk | Notes |
|---------|---------|---------|------|-------|
| PII stripping | `RVF_PII_STRIP` | `true` | Low | High value, replaces inline patterns |
| DP noise | `RVF_DP_ENABLED` | `false` | Medium | Enable after P@1 regression test |
| DP epsilon | `RVF_DP_EPSILON` | `1.0` | — | Privacy loss per memory |
| Witness chains | `RVF_WITNESS` | `true` | Low | Audit trail, no behavioral change |
| RVF containers | `RVF_CONTAINER` | `true` | Low | Upload .rvf to GCS |
| Adversarial detect | `RVF_ADVERSARIAL` | `false` | Medium | Log-only initially |
| Negative cache | `RVF_NEG_CACHE` | `false` | Medium | Enable after tuning threshold |
## 5. Backward Compatibility
- All new `BrainMemory` fields are `Option<T>` with `#[serde(default)]` — existing persisted memories deserialize cleanly
- Old `verify_no_pii()` / `verify_witness_chain()` methods kept for backward compat
- PII stripping adds redaction but does not change the rejection behavior for existing API clients
- Witness chain bytes stored alongside (not instead of) the existing `witness_hash` string field
- `ShareResponse` gains new fields — JSON clients ignore unknown fields by default
## 6. Files Summary
| File | Action | Phase |
|------|--------|-------|
| `crates/mcp-brain-server/Cargo.toml` | Add 5 rvf-* deps | 1 |
| `crates/mcp-brain-server/src/verify.rs` | Replace crypto, add PII strip, add adversarial detect | 1, 2, 4 |
| `crates/mcp-brain-server/src/routes.rs` | Wire PII strip, DP noise, witness chains, RVF build, neg cache | 2, 3, 4, 5, 6 |
| `crates/mcp-brain-server/src/types.rs` | Add fields to BrainMemory, AppState, StatusResponse, ShareResponse | 2, 3, 4, 6 |
| `crates/mcp-brain-server/src/pipeline.rs` | **New**: RVF container construction | 5 |
| `crates/mcp-brain-server/src/lib.rs` | Add `pub mod pipeline` | 5 |
## 7. Verification
1. `cargo build -p mcp-brain-server` — compiles with all rvf-* crates
2. `cargo test -p mcp-brain-server` — all existing tests pass + new tests:
- `test_rvf_witness_chain_roundtrip` — create 3-entry chain, verify integrity
- `test_pii_strip_redacts_paths` — `/home/user/data` → `<PATH_1>`
- `test_pii_strip_redacts_email` — `user@example.com` → `<EMAIL_1>`
- `test_dp_noise_changes_embedding` — same input produces different output
- `test_rvf_container_has_segments` — build container, count ≥ 3 segments
- `test_adversarial_degenerate_detection` — uniform distances flagged
3. Deploy to Cloud Run, verify `/v1/status` shows new fields (`dp_epsilon`, `rvf_segments_per_memory`)
4. POST a memory, verify response has `witness_hash`, `rvf_segments`
5. GET the memory back, verify `redaction_log` present if PII was stripped
6. Run P@1 benchmark to confirm no regression (DP disabled by default)
## 7.1 Phase 7: Hot-Path Performance Optimizations
After deployment validation, a deep review identified 7 per-request allocation and I/O bottlenecks. All were eliminated:
| Issue | Location | Impact | Fix |
|-------|----------|--------|-----|
| PiiStripper recompiles 12 regexes per call | `verify.rs:126` | 84 regex compiles per 5-tag request | Cache PiiStripper in `Verifier` struct |
| Verifier re-allocated per request | `routes.rs:337, 1206` | 2 allocations + 12 regex compiles per request | Shared `Arc<RwLock<Verifier>>` in AppState |
| 9 env::var reads per share request | `routes.rs` (6 locations) | 9 syscalls per write request | `RvfFeatureFlags::from_env()` at startup |
| Synonym HashMap allocated per search | `routes.rs:578-616` | 28-entry HashMap per search | `static LazyLock<HashMap>` (compiled once) |
| `all_memories()` called twice in status | `routes.rs:1018+1045` | 2x full DashMap clone per status request | Reuse single `all_memories` binding |
| `env::var("BRAIN_SYSTEM_KEY")` per auth | `auth.rs:71` | 1 syscall per authenticated request | `static LazyLock<String>` (read once) |
| Embedding bytes via flat_map (no pre-alloc) | `routes.rs:362` | Repeated small allocations in witness chain | `Vec::with_capacity(len * 4)` pre-allocation |
**Net effect**: Eliminates ~96 regex compilations + ~10 env::var syscalls + ~29 HashMap entries per write request. Search requests eliminate HashMap re-allocation entirely.
## 7.2 Phase 8: AGI Capability Wiring
After the RVF stack and hot-path optimizations, four AGI learning subsystems were wired into the brain server to enable adaptive intelligence:
### 8.1 SONA 3-Tier Learning (`SONA_ENABLED`, default: `true`)
Wire `sona::SonaEngine` for hierarchical pattern learning:
| Integration Point | Handler | Behavior |
|-------------------|---------|----------|
| Pattern re-ranking | `search_memories()` | Boost results matching learned patterns (cosine × quality × 0.15) |
| Trajectory tracking | `search_memories()` | Record search→result trajectories for online learning |
| Background learning | `status()` | Trigger periodic pattern consolidation via `sona.tick()` |
| Stats endpoint | `GET /v1/sona/stats` | Patterns stored, trajectories buffered, background ticks |
**AppState**: `sona: Arc<RwLock<SonaEngine>>` initialized with `SonaEngine::new(128)`.
### 8.2 Global Workspace Theory Attention (`GWT_ENABLED`, default: `true`)
Wire `ruvector_nervous_system::routing::workspace` for salience-based competition:
| Integration Point | Handler | Behavior |
|-------------------|---------|----------|
| Salience competition | `search_memories()` | Broadcast top 3×limit candidates, K-WTA competition selects winners |
| Attention boost | `search_memories()` | Winners get +0.1 score boost, results re-sorted by salience |
| Workspace load | `status()` | Report `gwt_workspace_load` (0.0-1.0) and `gwt_avg_salience` |
**AppState**: `workspace: Arc<RwLock<GlobalWorkspace>>` initialized with `GlobalWorkspace::with_threshold(7, 0.3)` (7-item capacity per Miller's Law).
### 8.3 Temporal Delta Tracking (`TEMPORAL_ENABLED`, default: `true`)
Wire `ruvector_delta_core::DeltaStream<VectorDelta>` for knowledge evolution tracking:
| Integration Point | Handler | Behavior |
|-------------------|---------|----------|
| Embedding delta | `share_memory()` | Push `VectorDelta::from_dense(embedding)` with timestamp |
| Vote delta | `vote_memory()` | Push vote signal (+1/-1) as delta event |
| Stats endpoint | `GET /v1/temporal` | Total deltas, recent-hour deltas, knowledge velocity, trend |
| Status fields | `status()` | Report `knowledge_velocity` (deltas/hour) and `temporal_deltas` |
**AppState**: `delta_stream: Arc<RwLock<DeltaStream<VectorDelta>>>` initialized with `DeltaStream::for_vectors(128)`.
### 8.4 Meta-Learning Exploration (`META_LEARNING_ENABLED`, default: `true`)
Wire `ruvector_domain_expansion::DomainExpansionEngine` meta-learning subsystem:
| Integration Point | Handler | Behavior |
|-------------------|---------|----------|
| Curiosity bonus | `search_memories()` | Boost under-explored categories by `novelty × 0.05` |
| Contribution recording | `share_memory()` | Record "contribute" arm decision with reward 0.5 |
| Vote reward | `vote_memory()` | Feed upvote=1.0/downvote=0.0 as reward on "search" arm |
| Explore endpoint | `GET /v1/explore` | Most curious category, regret summary, plateau status, health |
| Status fields | `status()` | Report `meta_avg_regret` and `meta_plateau_status` |
**AppState**: `domain_engine: Arc<RwLock<DomainExpansionEngine>>` (already existed, now actively used).
### 8.5 Updated Feature Gating Table
| Feature | Env Var | Default | Risk | Phase |
|---------|---------|---------|------|-------|
| PII stripping | `RVF_PII_STRIP` | `true` | Low | 2 |
| DP noise | `RVF_DP_ENABLED` | `false` | Medium | 3 |
| DP epsilon | `RVF_DP_EPSILON` | `1.0` | — | 3 |
| Witness chains | `RVF_WITNESS` | `true` | Low | 4 |
| RVF containers | `RVF_CONTAINER` | `true` | Low | 5 |
| Adversarial detect | `RVF_ADVERSARIAL` | `false` | Medium | 6 |
| Negative cache | `RVF_NEG_CACHE` | `false` | Medium | 6 |
| SONA learning | `SONA_ENABLED` | `true` | Low | 8 |
| GWT attention | `GWT_ENABLED` | `true` | Low | 8 |
| Temporal tracking | `TEMPORAL_ENABLED` | `true` | Low | 8 |
| Meta-learning | `META_LEARNING_ENABLED` | `true` | Low | 8 |
### 8.6 Updated StatusResponse Fields
| Field | Type | Phase | Purpose |
|-------|------|-------|---------|
| `dp_epsilon` | `f64` | 3 | Current DP epsilon parameter |
| `dp_budget_used` | `f64` | 3 | Fraction of privacy budget consumed |
| `rvf_segments_per_memory` | `f64` | 5 | Average segments per RVF container |
| `gwt_workspace_load` | `f32` | 8 | GWT attention workspace utilization |
| `gwt_avg_salience` | `f32` | 8 | Average salience of workspace representations |
| `knowledge_velocity` | `f64` | 8 | Embedding deltas per hour |
| `temporal_deltas` | `usize` | 8 | Total temporal deltas recorded |
| `sona_patterns` | `usize` | 8 | SONA patterns stored |
| `sona_trajectories` | `usize` | 8 | SONA trajectories buffered |
| `meta_avg_regret` | `f64` | 8 | Meta-learning average regret (lower = better) |
| `meta_plateau_status` | `String` | 8 | Meta-learning plateau status |
### 8.7 New Endpoints
| Endpoint | Method | Phase | Response |
|----------|--------|-------|----------|
| `/v1/sona/stats` | GET | 8 | Patterns, trajectories, background ticks |
| `/v1/temporal` | GET | 8 | Delta count, velocity, trend direction |
| `/v1/explore` | GET | 8 | Curiosity, regret, plateau, health diagnostics |
### 8.8 Dependencies (already present, now actively wired)
```toml
# Already in Cargo.toml — Phase 8 wires these into route handlers
sona = { package = "ruvector-sona", path = "../sona", features = ["serde-support"] }
ruvector-nervous-system = { path = "../ruvector-nervous-system" }
ruvector-delta-core = { path = "../ruvector-delta-core" }
ruvector-domain-expansion = { path = "../ruvector-domain-expansion" }
```
### 8.9 AGI Readiness Estimate
| Capability | Before Phase 8 | After Phase 8 |
|------------|----------------|---------------|
| Adaptive search ranking | Static cosine+keyword | SONA patterns + GWT attention + curiosity bonus |
| Knowledge evolution tracking | None | Temporal deltas, velocity, trend detection |
| Meta-cognitive awareness | None | Regret tracking, plateau detection, Pareto optimization |
| Self-directed exploration | None | Curiosity-driven category exploration |
| **Estimated AGI readiness** | **~40%** | **~65%** |
## 8. Consequences
### Positive
- Eliminates code duplication between inline crypto and production RVF crates
- Activates the full 12-rule PII stripping pipeline (was 8 simple patterns)
- Enables differential privacy for embedding protection (opt-in)
- Creates tamper-evident witness chains for every memory operation
- Produces real RVF containers stored in GCS for audit and federation
- Adds adversarial embedding detection (log-only)
- Adds negative cache to reduce cost of repeated degenerate queries
### Negative
- Five new path dependencies increase compile time (~15s incremental)
- DP noise (when enabled) will reduce embedding precision — requires P@1 benchmarking
- Witness chain adds ~219 bytes (3 × 73) per memory
- RVF container construction adds ~2ms latency per share operation
### Risks
- `DiffPrivacyEngine` uses thread-local RNG — `parking_lot::Mutex` serializes access; acceptable at current QPS
- `NegativeCache` false positives could block legitimate queries if threshold is too low — start with `threshold=5`
- PII stripping regex rules may be too aggressive for some content types — monitor false positive rate via `RedactionLog`
## 9. Related ADRs
| ADR | Relationship |
|-----|-------------|
| ADR-029 | RVF Canonical Format — wire format specification |
| ADR-057 | Federated RVF Transfer Learning — federation protocol |
| ADR-058 | Hash Security Optimization — SHAKE-256 content hashing used by witness chains |
| ADR-059 | Shared Brain Google Cloud — infrastructure, deployment, module migration map |
| ADR-060 | Shared Brain Capabilities — 10-segment container layout, threat model |
| ADR-068 | Domain Expansion Transfer Learning — meta-learning engine |
| ADR-074 | RuvLLM Neural Embeddings — embedding engine |
| ADR-076 | AGI Capability Wiring Architecture — Phase 8 architecture decisions |

View file

@ -0,0 +1,174 @@
# ADR-076: AGI Capability Wiring Architecture
**Status**: Implemented
**Date**: 2026-03-03
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-075 (RVF AGI Stack Brain Integration), ADR-068 (Domain Expansion Transfer Learning), ADR-074 (RuvLLM Neural Embeddings)
## 1. Context
The mcp-brain-server at pi.ruv.io had four AGI subsystem crates available in the workspace but minimally integrated:
- **SONA** (`sona`): 3-tier hierarchical learning engine with pattern detection and trajectory tracking
- **Global Workspace Theory** (`ruvector-nervous-system`): Salience-based attention competition inspired by cognitive GWT
- **Temporal Delta Tracking** (`ruvector-delta-core`): Time-series delta streams for tracking embedding evolution
- **Meta-Learning Exploration** (`ruvector-domain-expansion`): Thompson Sampling meta-learning with curiosity, regret, and plateau detection
Each crate had comprehensive unit tests but no integration with the live brain server. The `DomainExpansionEngine` was in `AppState` but only touched by the transfer endpoint. The other three subsystems were not in AppState at all.
## 2. Decision
Wire all four AGI subsystems into the brain server's core handlers (`share_memory`, `search_memories`, `vote_memory`, `status`) with independent feature flags for gradual rollout. Each subsystem adds a distinct cognitive capability without disrupting existing search/ranking behavior.
### 2.1 Architecture Principle: Additive Scoring Layers
Each AGI subsystem contributes a small additive score adjustment to the existing hybrid ranking pipeline:
```
Base score: keyword_boost + cosine_similarity + graph_ppr + reputation + vote_quality
+ SONA pattern boost: cosine(mem, pattern) * quality * 0.15
+ GWT attention boost: +0.1 for workspace competition winners
+ Meta curiosity boost: novelty_score * 0.05
```
The small coefficients (0.05-0.15) ensure no single subsystem can dominate ranking, while still providing measurable signal from each capability.
### 2.2 Architecture Principle: Read-Lock Scoring, Write-Lock Learning
All scoring operations (search) use read locks on AGI state. Learning operations (share, vote) use write locks. This minimizes contention:
| Operation | SONA | GWT | DeltaStream | MetaLearning |
|-----------|------|-----|-------------|--------------|
| search (score) | read | write (compete) | - | read |
| share (learn) | - | - | write | write |
| vote (learn) | - | - | write | write |
| status (report) | read | read | read | read |
GWT is the exception: `compete()` mutates workspace state during search. This is intentional — attention competition is inherently stateful.
### 2.3 Architecture Principle: Feature-Gated with Defaults On
All four subsystems default to enabled. This is safe because:
1. Score contributions are small (0.05-0.15) and additive
2. Each subsystem starts with no learned state (cold start = no effect)
3. Feature flags allow instant disable without redeployment via env vars
4. Subsystems learn passively from existing traffic — no active exploration that could degrade quality
### 2.4 Handler Integration Map
```
share_memory() flow:
1. [existing] PII strip, embed, witness chain, RVF container
2. [Phase 8] Push VectorDelta to DeltaStream (temporal)
3. [Phase 8] Record "contribute" decision in MetaLearningEngine
4. [existing] Add to graph, store in Firestore
search_memories() flow:
1. [existing] Embed query, fetch candidates, keyword+cosine scoring
2. [existing] RankingEngine attention adjustments
3. [Phase 8] GWT salience competition (broadcast → compete → boost winners)
4. [Phase 8] SONA pattern re-ranking (centroid similarity × quality)
5. [Phase 8] Meta-learning curiosity bonus (novelty_score × 0.05)
6. [existing] Truncate to limit
7. [Phase 8] SONA trajectory recording (search→result for online learning)
vote_memory() flow:
1. [existing] Quality update, reputation, poisoning check
2. [Phase 8] Push vote delta to DeltaStream (temporal)
3. [Phase 8] Feed vote as reward signal to MetaLearningEngine
4. [existing] Record contribution
```
## 3. New Endpoints
### GET /v1/sona/stats
Returns SONA learning engine statistics:
```json
{
"patterns_stored": 12,
"trajectories_buffered": 45,
"background_ticks": 3
}
```
### GET /v1/temporal
Returns temporal delta tracking statistics:
```json
{
"total_deltas": 237,
"recent_hour_deltas": 14,
"knowledge_velocity": 14.0,
"trend": "growing"
}
```
### GET /v1/explore
Returns meta-learning exploration diagnostics:
```json
{
"most_curious_category": "security",
"most_curious_novelty": 0.92,
"regret_summary": {
"total_regret": 0.0,
"average_regret": 0.0,
"mean_growth_rate": 1.0,
"converged_buckets": 0,
"bucket_count": 0,
"total_observations": 0
},
"plateau_status": "learning",
"is_learning": false,
"is_diverse": false,
"is_exploring": false,
"curiosity_total_visits": 0,
"pareto_size": 0
}
```
## 4. AppState Additions
| Field | Type | Subsystem |
|-------|------|-----------|
| `sona` | `Arc<RwLock<SonaEngine>>` | SONA 3-tier learning |
| `workspace` | `Arc<RwLock<GlobalWorkspace>>` | GWT attention |
| `delta_stream` | `Arc<RwLock<DeltaStream<VectorDelta>>>` | Temporal tracking |
| `domain_engine` | `Arc<RwLock<DomainExpansionEngine>>` | Meta-learning (pre-existing) |
## 5. Consequences
### Positive
- Brain server now has four distinct AGI learning capabilities operating in production
- Search ranking benefits from multi-signal fusion: patterns, attention, curiosity, keywords
- Knowledge evolution is tracked over time, enabling trend detection and velocity monitoring
- Meta-learning provides self-diagnostic capabilities (regret, plateau, Pareto optimization)
- All capabilities are feature-gated for safe gradual rollout
- Cold-start behavior is neutral (no learned state = no effect on ranking)
### Negative
- Four additional read/write locks in the search path increase contention potential
- GWT workspace mutation during search is a sequential bottleneck
- Each subsystem adds ~1-5ms to search latency (total ~5-15ms)
- Memory footprint increases by ~2-8MB for AGI state (patterns, workspace, delta stream)
### Risks
- SONA pattern learning may create feedback loops (popular patterns get more popular)
- GWT K-WTA competition with small candidate sets may not produce meaningful selection
- Meta-learning curiosity bonus may be too small (0.05) to noticeably affect ranking
- Temporal delta stream grows unbounded without periodic compaction — needs future cleanup
## 6. Verification
1. `cargo check` from `crates/mcp-brain-server/` compiles with zero errors
2. All Phase 1-7 tests continue to pass
3. `/v1/status` returns new fields: `sona_patterns`, `gwt_workspace_load`, `knowledge_velocity`, `meta_avg_regret`, `meta_plateau_status`
4. `/v1/explore`, `/v1/temporal`, `/v1/sona/stats` return valid JSON
5. Feature flags disable each subsystem independently without affecting others

View file

@ -0,0 +1,914 @@
# ADR-077: Midstream Platform Integration into mcp-brain-server
**Status**: Proposed
**Date**: 2026-03-03
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-076 (AGI Capability Wiring Architecture), ADR-075 (RVF AGI Stack Brain Integration), ADR-068 (Domain Expansion Transfer Learning)
## 1. Context
The mcp-brain-server at pi.ruv.io is a production axum REST API on Cloud Run with 238+ shared memories, Phase 8 AGI subsystems (SONA, GWT, DeltaStream, DomainExpansionEngine), an RVF security stack, and a hybrid keyword+cosine+graph+reputation scoring pipeline. Current read latency is 60-80ms and write latency is 150ms at 40-concurrent p90=172ms.
Despite this sophistication, several capability gaps remain:
1. **No temporal pattern matching on embedding evolution**. The DeltaStream records embedding deltas over time, but there is no way to find memories whose embedding trajectories are *similar* to each other. Two knowledge nodes may evolve the same way (both drifting toward security topics, for instance) but the system cannot detect this.
2. **No deadline-aware background task scheduling**. AGI subsystems (SONA tick, GWT decay, delta compaction) run ad-hoc inside request handlers or on status checks. There is no priority-based scheduler guaranteeing that critical maintenance (e.g., compaction before Firestore timeout) completes before less urgent work (e.g., pattern re-indexing).
3. **No dynamical systems analysis of knowledge drift**. The DriftMonitor computes coefficient-of-variation but cannot classify whether the knowledge graph is converging to a stable attractor, oscillating in a limit cycle, or exhibiting chaotic drift. Lyapunov exponent analysis would provide this.
4. **No formal invariant verification**. Memory operations have implicit invariants (witness chain integrity, embedding dimension consistency, quality score monotonicity under upvotes) that are checked procedurally. Linear Temporal Logic (LTL) verification would provide formal guarantees.
5. **No recursive meta-cognition with safety bounds**. The DomainExpansionEngine does Thompson Sampling meta-learning but cannot recursively reason about its own learning strategy. Self-modification (changing exploration parameters based on accumulated regret) is manually tuned.
6. **No high-performance brain-to-brain transport**. Federation currently uses HTTP/1.1 REST via reqwest. A future multi-brain mesh needs multiplexed, 0-RTT transport for real-time knowledge synchronization.
The [midstream platform](https://github.com/ruvnet/midstream) provides six crates that address each gap precisely.
## 2. Decision
Integrate all six midstream crates into mcp-brain-server in a phased rollout, each behind an independent feature flag (env var). Every crate maps to a specific gap identified in Section 1 and wires into existing handlers without disrupting the current scoring pipeline.
### 2.1 Crate-to-Gap Mapping
| Crate | Gap Addressed | Integration Target |
|-------|--------------|-------------------|
| `temporal-compare` | Embedding evolution similarity | `search_memories()` DTW-based trajectory matching |
| `nanosecond-scheduler` | Background task scheduling | SONA tick, GWT decay, delta compaction, LTL checks |
| `temporal-attractor-studio` | Dynamical systems drift analysis | Knowledge drift classification (chaotic vs stable) |
| `temporal-neural-solver` | Formal invariant verification | LTL properties on memory operations |
| `strange-loop` | Recursive meta-cognition | Self-modifying meta-learning atop DomainExpansionEngine |
| `quic-multistream` | Brain-to-brain federation | Future multi-brain mesh transport |
### 2.2 Architecture Principle: Layered Composition
Midstream crates compose *on top of* existing AGI subsystems rather than replacing them:
```
Layer 4 (Midstream Meta): strange-loop (recursive self-improvement)
Layer 3 (Midstream Analysis): temporal-attractor-studio + temporal-neural-solver
Layer 2 (Midstream Infra): nanosecond-scheduler + temporal-compare
Layer 1 (Phase 8 AGI): SONA + GWT + DeltaStream + DomainExpansionEngine
Layer 0 (Core): Firestore + KnowledgeGraph + RVF + Embeddings
```
No midstream crate touches Layer 0 directly. All access goes through Layer 1 state via `AppState`.
### 2.3 Architecture Principle: Additive Scoring with Bounded Coefficients
Following ADR-076's precedent, midstream scoring signals are small and additive:
```
Existing scoring pipeline (unchanged):
keyword_boost + cosine_sim + graph_ppr + reputation + vote_quality
+ SONA pattern boost (0.15)
+ GWT attention boost (0.1)
+ Meta curiosity boost (0.05)
New midstream additions:
+ temporal_compare DTW similarity boost: dtw_score * 0.08
+ attractor stability boost: stability_bonus * 0.03
+ strange_loop confidence boost: confidence * 0.02
```
Total midstream contribution is bounded to 0.13, well below the existing AGI layer total of 0.30. This ensures midstream can never dominate ranking.
### 2.4 Architecture Principle: Scheduler-Mediated Background Work
All periodic background tasks (previously scattered across handlers) consolidate under `nanosecond-scheduler`:
| Task | Priority | Deadline | Current Location | New Location |
|------|----------|----------|-----------------|-------------|
| SONA tick | Medium(50) | 50ms | `status()` handler | Scheduler (10s interval) |
| GWT decay/compete | Medium(50) | 20ms | `search_memories()` | Scheduler (5s interval) |
| DeltaStream compaction | Low(25) | 200ms | Never (unbounded growth) | Scheduler (60s interval) |
| LTL invariant checks | Low(25) | 100ms | Never | Scheduler (30s interval) |
| Attractor analysis | Background(10) | 500ms | Never | Scheduler (120s interval) |
| Strange-loop reflection | Background(10) | 1000ms | Never | Scheduler (300s interval) |
This removes AGI maintenance work from the request hot path, directly reducing read latency.
## 3. Handler Integration Map
### 3.1 share_memory() Flow (Write Path)
```
share_memory() flow:
1. [existing] PII strip, embed, witness chain, RVF container
2. [existing] Push VectorDelta to DeltaStream
3. [existing] Record "contribute" decision in MetaLearningEngine
4. [NEW: Phase 9a] Append embedding to TemporalComparator trajectory buffer
5. [NEW: Phase 9c] Push PhasePoint to AttractorAnalyzer (category-scoped)
6. [NEW: Phase 9d] Add state to TemporalNeuralSolver (witness_valid, dim_correct)
7. [existing] Add to graph, store in Firestore
```
Steps 4-6 are non-blocking appends to in-memory buffers. No additional latency on the write path.
### 3.2 search_memories() Flow (Read Path)
```
search_memories() flow:
1. [existing] Embed query, fetch candidates, keyword+cosine scoring
2. [existing] RankingEngine attention adjustments
3. [existing] GWT salience competition
4. [existing] SONA pattern re-ranking
5. [existing] Meta-learning curiosity bonus
6. [NEW: Phase 9a] Temporal trajectory similarity boost (DTW)
7. [NEW: Phase 9c] Attractor stability boost
8. [NEW: Phase 9e] Strange-loop confidence boost
9. [existing] Final sort, truncate to limit
10. [existing] SONA trajectory recording
```
Steps 6-8 use read locks only and add bounded scoring signals.
### 3.3 vote_memory() Flow
```
vote_memory() flow:
1. [existing] Quality update, reputation, poisoning check
2. [existing] Push vote delta to DeltaStream
3. [existing] Feed vote as reward to MetaLearningEngine
4. [NEW: Phase 9e] Feed vote as reward signal to StrangeLoop (meta-level feedback)
5. [NEW: Phase 9d] Record vote_quality_increased proposition in LTL solver
```
Steps 4-5 are fire-and-forget writes to in-memory state.
### 3.4 status() Flow
```
status() flow:
1. [existing] Graph stats, quality, drift, DP, SONA, GWT, temporal, meta stats
2. [NEW: Phase 9b] Scheduler stats (tasks completed, missed deadlines, avg latency)
3. [NEW: Phase 9c] Attractor classification (point/limit_cycle/strange) per category
4. [NEW: Phase 9d] LTL invariant health (properties verified, violations detected)
5. [NEW: Phase 9e] Strange-loop meta-knowledge summary (depth, confidence)
6. [NEW: Phase 9f] Federation transport stats (if quic-multistream active)
```
## 4. AppState Additions
| Field | Type | Crate | Description |
|-------|------|-------|-------------|
| `temporal_comparator` | `Arc<RwLock<temporal_compare::TemporalComparator<f32>>>` | temporal-compare | DTW/LCS/EditDistance on embedding sequences |
| `scheduler` | `Arc<RwLock<nanosecond_scheduler::RealtimeScheduler<SchedulerPayload>>>` | nanosecond-scheduler | Priority-based background task executor |
| `attractor_analyzers` | `Arc<RwLock<HashMap<String, temporal_attractor_studio::AttractorAnalyzer>>>` | temporal-attractor-studio | Per-category dynamical systems analysis |
| `ltl_solver` | `Arc<RwLock<temporal_neural_solver::TemporalNeuralSolver>>` | temporal-neural-solver | LTL property verification engine |
| `strange_loop` | `Arc<RwLock<strange_loop::StrangeLoop>>` | strange-loop | Meta-cognitive recursive learning |
| `quic_stats` | `Arc<RwLock<Option<QuicFederationStats>>>` | quic-multistream | Federation transport statistics (Phase 9f only) |
### 4.1 Scheduler Payload Type
```rust
/// Payload for scheduled background tasks.
#[derive(Debug, Clone)]
pub enum SchedulerPayload {
SonaTick,
GwtDecay,
DeltaCompact { max_entries: usize },
LtlCheck,
AttractorAnalyze { category: String },
StrangeLoopReflect,
}
```
### 4.2 QuicFederationStats Type
```rust
/// Statistics from quic-multistream federation transport.
#[derive(Debug, Clone, Default, Serialize)]
pub struct QuicFederationStats {
pub active_peers: usize,
pub active_streams: usize,
pub bytes_sent: u64,
pub bytes_received: u64,
pub avg_rtt_ms: f64,
}
```
## 5. New Endpoints
### GET /v1/temporal/trajectories
Returns temporal trajectory similarity analysis for a given memory.
**Query Parameters**:
- `memory_id` (required): UUID of the memory to find similar trajectories for
- `algorithm` (optional): `dtw` | `lcs` | `edit_distance` (default: `dtw`)
- `threshold` (optional): minimum similarity (default: 0.7)
- `limit` (optional): max results (default: 5)
**Response**:
```json
{
"query_memory_id": "550e8400-e29b-41d4-a716-446655440000",
"algorithm": "dtw",
"similar_trajectories": [
{
"memory_id": "660e8400-e29b-41d4-a716-446655440001",
"distance": 0.12,
"similarity": 0.88,
"alignment_length": 15
}
],
"recurring_patterns": [
{
"pattern_length": 8,
"occurrences": 3,
"confidence": 0.92
}
]
}
```
### GET /v1/scheduler/stats
Returns background task scheduler statistics.
**Response**:
```json
{
"total_scheduled": 1247,
"total_completed": 1240,
"missed_deadlines": 2,
"avg_latency_ns": 45000,
"tasks_by_priority": {
"critical": 0,
"high": 12,
"medium": 820,
"low": 408,
"background": 7
},
"next_deadline_ms": 4200
}
```
### GET /v1/attractor
Returns dynamical systems analysis of knowledge drift per category.
**Query Parameters**:
- `category` (optional): specific category to analyze (default: all)
**Response**:
```json
{
"categories": {
"architecture": {
"attractor_type": "point_attractor",
"lyapunov_exponents": [-0.32, -0.18, -0.05],
"is_stable": true,
"is_chaotic": false,
"confidence": 0.94,
"trajectory_length": 45,
"mean_velocity": 0.023
},
"security": {
"attractor_type": "strange_attractor",
"lyapunov_exponents": [0.12, -0.04, -0.21],
"is_stable": false,
"is_chaotic": true,
"confidence": 0.78,
"trajectory_length": 32,
"mean_velocity": 0.089
}
},
"global_stability": "mixed"
}
```
### GET /v1/invariants
Returns LTL invariant verification status.
**Response**:
```json
{
"properties_defined": 5,
"properties_verified": 4,
"violations": [
{
"property": "globally(embedding_dim_128)",
"satisfied": false,
"confidence": 0.99,
"counterexample": "memory abc123 has dim=64 at state 17"
}
],
"last_check_ms": 12,
"total_states_checked": 238
}
```
### GET /v1/meta/strange-loop
Returns strange-loop meta-cognitive status.
**Response**:
```json
{
"meta_depth": 2,
"knowledge_items": 7,
"top_patterns": [
{
"level": 1,
"pattern": "security_memories_converge_faster",
"confidence": 0.87,
"applications": ["adjust_curiosity_weight", "increase_security_exploration"]
}
],
"self_modifications_applied": 3,
"safety_constraints_active": 2,
"is_self_modification_enabled": true
}
```
### GET /v1/federation/stats
Returns QUIC federation transport statistics (Phase 9f only).
**Response**:
```json
{
"transport": "quic-h3",
"active_peers": 0,
"active_streams": 0,
"bytes_sent": 0,
"bytes_received": 0,
"avg_rtt_ms": 0.0,
"zero_rtt_supported": true,
"status": "standby"
}
```
## 6. Feature Gating
All midstream subsystems are gated by environment variables, read once at startup via `RvfFeatureFlags::from_env()`. All default to disabled (opt-in) because midstream crates add new dependencies and should be validated incrementally.
| Env Var | Default | Controls |
|---------|---------|----------|
| `MIDSTREAM_TEMPORAL_COMPARE` | `false` | DTW trajectory matching in search |
| `MIDSTREAM_SCHEDULER` | `false` | Background task scheduler |
| `MIDSTREAM_ATTRACTOR` | `false` | Dynamical systems drift analysis |
| `MIDSTREAM_LTL` | `false` | LTL invariant verification |
| `MIDSTREAM_STRANGE_LOOP` | `false` | Recursive meta-cognition |
| `MIDSTREAM_QUIC` | `false` | QUIC federation transport |
### 6.1 RvfFeatureFlags Additions
```rust
// Add to existing RvfFeatureFlags struct:
pub midstream_temporal_compare: bool,
pub midstream_scheduler: bool,
pub midstream_attractor: bool,
pub midstream_ltl: bool,
pub midstream_strange_loop: bool,
pub midstream_quic: bool,
```
```rust
// Add to from_env():
midstream_temporal_compare: std::env::var("MIDSTREAM_TEMPORAL_COMPARE")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
midstream_scheduler: std::env::var("MIDSTREAM_SCHEDULER")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
midstream_attractor: std::env::var("MIDSTREAM_ATTRACTOR")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
midstream_ltl: std::env::var("MIDSTREAM_LTL")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
midstream_strange_loop: std::env::var("MIDSTREAM_STRANGE_LOOP")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
midstream_quic: std::env::var("MIDSTREAM_QUIC")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
```
### 6.2 Gradual Rollout Strategy
1. Deploy with all flags `false` -- baseline performance unchanged
2. Enable `MIDSTREAM_SCHEDULER=true` first (moves AGI maintenance off hot path)
3. Enable `MIDSTREAM_TEMPORAL_COMPARE=true` (new search signal)
4. Enable `MIDSTREAM_ATTRACTOR=true` (drift analysis)
5. Enable `MIDSTREAM_LTL=true` (invariant checking)
6. Enable `MIDSTREAM_STRANGE_LOOP=true` (meta-cognition)
7. Enable `MIDSTREAM_QUIC=true` only when peer brains exist
## 7. Scoring Pipeline Update
The full scoring pipeline after midstream integration, showing exact order and coefficients:
```
search_memories() scoring pipeline:
1. Base hybrid score (existing, unchanged):
IF keyword_match:
score = 1.0 + keyword_boost * 0.85 + vec_sim * 0.05
+ graph_ppr * 0.04 + reputation * 0.03 + vote_boost * 0.03
ELSE:
score = vec_sim * 0.45 + graph_ppr * 0.25
+ reputation * 0.15 + vote_boost * 0.15
2. RankingEngine adjustments (existing, unchanged):
score = similarity_weight(0.85) * score
+ quality_weight(0.10) * quality_mean
+ recency_weight(0.05) * recency_factor
3. GWT attention (existing, unchanged):
score += 0.10 for workspace competition winners
score += sparse_activation * 0.05 for K-WTA
4. SONA pattern (existing, unchanged):
score += avg(cosine(mem, pattern) * pattern_quality) * 0.15
5. Meta-learning curiosity (existing, unchanged):
score += novelty_score * 0.05
6. [NEW] Temporal trajectory similarity (Phase 9a):
IF MIDSTREAM_TEMPORAL_COMPARE enabled AND query has temporal context:
dtw_score = temporal_comparator.find_similar(query_trajectory, threshold=0.7)
score += dtw_score * 0.08 for memories with similar evolution paths
7. [NEW] Attractor stability bonus (Phase 9c):
IF MIDSTREAM_ATTRACTOR enabled:
attractor = attractor_analyzers[memory.category].analyze()
IF attractor.is_stable AND attractor.confidence > 0.8:
score += 0.03 // prefer memories in stable knowledge regions
8. [NEW] Strange-loop confidence bonus (Phase 9e):
IF MIDSTREAM_STRANGE_LOOP enabled:
meta_knowledge = strange_loop.learn_at_level(0, query_context)
IF any meta_knowledge has confidence > 0.9:
score += meta_knowledge.confidence * 0.02
9. Final sort (single unstable_sort_by descending score)
10. Truncate to limit
```
### 7.1 Coefficient Budget
| Layer | Max Contribution | Source |
|-------|-----------------|--------|
| Keyword boost | 3.0 | ADR-076 (unchanged) |
| Vector similarity | 0.45 | Core scoring |
| Graph PPR | 0.25 | Core scoring |
| Reputation + votes | 0.30 | Core scoring |
| RankingEngine | ~1.0 | Quality/recency blend |
| GWT attention | 0.15 | ADR-076 |
| SONA patterns | 0.15 | ADR-076 |
| Meta curiosity | 0.05 | ADR-076 |
| **Temporal compare** | **0.08** | **Phase 9a (new)** |
| **Attractor stability** | **0.03** | **Phase 9c (new)** |
| **Strange-loop** | **0.02** | **Phase 9e (new)** |
| **Midstream total** | **0.13** | **Sum of new layers** |
## 8. Implementation Phases
### Phase 9a: temporal-compare Integration (Week 1)
**Dependency**: None (standalone crate)
**Wiring**:
1. Add `temporal_comparator: Arc<RwLock<TemporalComparator<f32>>>` to AppState
2. Initialize with `TemporalComparator::new(1000, 500)` (cache 1000 comparisons, max 500-step sequences)
3. In `share_memory()`: after DeltaStream push, append embedding to per-memory trajectory buffer in comparator
4. In `search_memories()`: after SONA re-ranking, use `find_similar_generic()` to boost memories with similar embedding evolution trajectories (DTW distance < 0.3 threshold)
5. Add `GET /v1/temporal/trajectories` endpoint
6. Gate all with `MIDSTREAM_TEMPORAL_COMPARE` env var
**Data flow**:
```
share_memory() -> embedding -> temporal_comparator.trajectory_buffer[memory_id].push(embedding)
search_memories() -> query_trajectory -> find_similar_generic(candidates, query, 0.7) -> score += dtw * 0.08
```
**Latency budget**: +3ms read path (DTW on cached trajectories), +0ms write path (buffer append)
### Phase 9b: nanosecond-scheduler Integration (Week 1-2)
**Dependency**: None (standalone), but should be enabled before other phases to manage their background tasks
**Wiring**:
1. Add `scheduler: Arc<RwLock<RealtimeScheduler<SchedulerPayload>>>` to AppState
2. Initialize with EDF (Earliest Deadline First) policy
3. Spawn a `tokio::spawn` loop that calls `scheduler.next_task()` and dispatches:
- `SonaTick` -> `state.sona.read().tick()`
- `GwtDecay` -> `state.workspace.write().compete()`
- `DeltaCompact` -> `state.delta_stream.write().compact(max_entries)`
- `LtlCheck` -> `state.ltl_solver.write().verify(invariants)`
- `AttractorAnalyze` -> `state.attractor_analyzers.write()[cat].analyze()`
- `StrangeLoopReflect` -> `state.strange_loop.write().learn_at_level(0, data)`
4. Remove SONA tick from `status()` handler (moved to scheduler)
5. Add `GET /v1/scheduler/stats` endpoint
6. Gate with `MIDSTREAM_SCHEDULER` env var
**Scheduler task registration at startup**:
```rust
// Register recurring tasks with appropriate priorities and intervals
let mut sched = scheduler.write();
sched.schedule(SchedulerPayload::SonaTick, deadline_10s, Priority::Medium(50));
sched.schedule(SchedulerPayload::GwtDecay, deadline_5s, Priority::Medium(50));
sched.schedule(SchedulerPayload::DeltaCompact { max_entries: 10_000 },
deadline_60s, Priority::Low(25));
```
**Latency budget**: -5ms read path (SONA tick removed from status), +0ms (scheduler runs in background task)
### Phase 9c: temporal-attractor-studio Integration (Week 2)
**Dependency**: Phase 9b (scheduler runs attractor analysis in background)
**Wiring**:
1. Add `attractor_analyzers: Arc<RwLock<HashMap<String, AttractorAnalyzer>>>` to AppState
2. Initialize one `AttractorAnalyzer::new(128, 1000)` per known category (embedding_dim=128, max_trajectory=1000)
3. In `share_memory()`: after DeltaStream push, call `analyzer.add_point(PhasePoint { coordinates: embedding, timestamp })` for the memory's category
4. Background (via scheduler): every 120s, call `analyzer.analyze()` for each category to get `AttractorInfo`
5. In `search_memories()`: if `attractor.is_stable && confidence > 0.8`, add `+0.03` stability bonus to memories in that category
6. In `status()`: report `attractor_type` and `is_chaotic` per category
7. Add `GET /v1/attractor` endpoint
8. Gate with `MIDSTREAM_ATTRACTOR` env var
**Attractor classification interpretation**:
- `PointAttractor` (all Lyapunov exponents < 0): knowledge is converging -- stable domain, prefer these memories
- `LimitCycle` (one exponent = 0, rest < 0): knowledge oscillates -- possibly seasonal patterns
- `StrangeAttractor` (at least one exponent > 0): chaotic evolution -- flag for human review, reduce score weight
**Latency budget**: +1ms read path (HashMap lookup + cached AttractorInfo read), +0ms write path (point append)
### Phase 9d: temporal-neural-solver Integration (Week 3)
**Dependency**: Phase 9b (scheduler runs LTL checks in background)
**Wiring**:
1. Add `ltl_solver: Arc<RwLock<TemporalNeuralSolver>>` to AppState
2. Initialize with `TemporalNeuralSolver::new(10_000, 100, Strictness::Medium)` (max 10K trace, 100ms timeout)
3. Define invariant properties at startup:
```rust
use temporal_neural_solver::formula::*;
// Property 1: Embedding dimension is always 128
let dim_128 = globally(atom("embedding_dim_128"));
// Property 2: Quality score never decreases after upvote
let quality_monotone = globally(
implies(atom("upvote_applied"), finally(atom("quality_increased")))
);
// Property 3: Witness chain is always present when RVF enabled
let witness_present = globally(
implies(atom("rvf_enabled"), atom("witness_chain_present"))
);
// Property 4: PII stripping happens before embedding
let pii_before_embed = globally(
implies(atom("has_pii"), until(atom("pii_stripped"), atom("embedded")))
);
// Property 5: No memory exists in both graph and negative cache
let no_blacklisted_in_graph = globally(
not(and(atom("in_graph"), atom("in_negative_cache")))
);
```
4. In `share_memory()`: push `TemporalState` with propositions `{embedding_dim_128: dim==128, witness_chain_present: chain.is_some(), ...}`
5. In `vote_memory()`: push `TemporalState` with `{upvote_applied: true, quality_increased: new > old}`
6. Background (via scheduler): every 30s, verify all properties and log violations
7. Add `GET /v1/invariants` endpoint
8. Gate with `MIDSTREAM_LTL` env var
**On violation**: log at WARN level, increment violation counter, but do not block operations. LTL verification is observational, not enforcement.
**Latency budget**: +0ms read path (verification runs in background only), +0.5ms write path (state push)
### Phase 9e: strange-loop Integration (Week 3-4)
**Dependency**: Phase 9b (scheduler), Phase 9c (attractor data feeds meta-learning), DomainExpansionEngine (existing)
**Wiring**:
1. Add `strange_loop: Arc<RwLock<StrangeLoop>>` to AppState
2. Initialize with:
```rust
let config = StrangeLoopConfig {
max_meta_depth: 3, // Max 3 levels of recursive reflection
enable_self_modification: true,
safety_check: true,
};
let mut sl = StrangeLoop::new(config);
// Safety constraints
sl.add_safety_constraint(always_safe()); // Never produce unsafe state
sl.add_safety_constraint(eventually_terminates()); // Always halt
// Custom: curiosity weight must stay in [0.01, 0.20]
sl.add_safety_constraint(custom_constraint(
"curiosity_bounds",
|state| state.curiosity_weight >= 0.01 && state.curiosity_weight <= 0.20
));
// Custom: meta-depth must not exceed config limit
sl.add_safety_constraint(custom_constraint(
"depth_limit",
|state| state.current_depth <= 3
));
```
3. Background (via scheduler): every 300s, run `learn_at_level(0, trajectory_data)` where trajectory_data comes from:
- SONA trajectory stats (patterns found, quality distribution)
- DomainExpansion regret history (per-category regret)
- Attractor stability classification (from Phase 9c)
4. Meta-knowledge application: when `MetaKnowledge` items have confidence > 0.9, apply modifications:
- `adjust_curiosity_weight(delta)` -> modify DomainExpansionEngine's exploration factor
- `adjust_sona_pattern_boost(delta)` -> modify SONA scoring coefficient (0.15 +/- 0.03)
- `flag_category_for_review(category)` -> mark chaotic categories for human attention
5. In `search_memories()`: if strange-loop has high-confidence meta-knowledge about the query category, add `confidence * 0.02` boost
6. In `vote_memory()`: feed vote as reward to strange-loop at level 0 (meta-reward: "did the meta-knowledge improve search quality?")
7. Add `GET /v1/meta/strange-loop` endpoint
8. Gate with `MIDSTREAM_STRANGE_LOOP` env var
**Self-modification bounds** (enforced by safety constraints):
- Curiosity weight: [0.01, 0.20] (cannot disable exploration or make it dominate)
- SONA pattern boost: [0.05, 0.25] (cannot disable patterns or make them dominate)
- Meta-depth: maximum 3 (prevents infinite recursion)
- Modification rate: maximum 1 modification per 300s cycle (prevents oscillation)
**Latency budget**: +1ms read path (cached meta-knowledge lookup), +0ms write path (async reward recording)
### Phase 9f: quic-multistream Integration (Week 4-5, Future)
**Dependency**: All prior phases (federation transports the full brain state including midstream data)
**Wiring**:
1. Add `quic_stats: Arc<RwLock<Option<QuicFederationStats>>>` to AppState
2. When `MIDSTREAM_QUIC=true`, initialize QUIC listener alongside the existing HTTP server:
```rust
// Separate listener for QUIC federation (port 4433 default)
let quic_port: u16 = std::env::var("QUIC_PORT")
.unwrap_or_else(|_| "4433".to_string())
.parse()?;
```
3. Define stream priority mapping:
- `Critical`: Memory write replication (consistency)
- `High`: Search query federation (latency-sensitive)
- `Normal`: Vote synchronization
- `Low`: Background state sync (attractor data, meta-knowledge)
4. 0-RTT for returning peer brains (session resumption avoids TLS handshake on reconnect)
5. Multiplexed bi-directional streams: each memory category gets its own stream for parallel sync
6. In `status()`: report `quic_stats` if transport is active
7. Add `GET /v1/federation/stats` endpoint
8. Gate with `MIDSTREAM_QUIC` env var
**Latency budget**: +0ms for existing endpoints (QUIC runs on separate port/task), +2ms for federated search (0-RTT + multiplexing)
## 9. Cargo.toml Changes
Add to `crates/mcp-brain-server/Cargo.toml` under `[dependencies]`:
```toml
# Midstream Platform (ADR-077)
temporal-compare = "0.1"
nanosecond-scheduler = "0.1"
temporal-attractor-studio = "0.1"
temporal-neural-solver = "0.1"
strange-loop = "0.1"
quic-multistream = { version = "0.1", optional = true }
```
Add feature flags for optional heavy dependencies:
```toml
[features]
default = []
quic-federation = ["quic-multistream"]
```
`quic-multistream` is the only optional dependency because it pulls in quinn/rustls which significantly increases compile time and binary size. All other midstream crates are lightweight and always compiled (gated at runtime via env vars).
## 10. Performance Budget
### 10.1 Read Path (search_memories) Latency Impact
| Component | Current | With Midstream | Delta |
|-----------|---------|---------------|-------|
| Embedding + candidate fetch | 15ms | 15ms | +0ms |
| Keyword + cosine scoring | 10ms | 10ms | +0ms |
| Graph PPR | 8ms | 8ms | +0ms |
| RankingEngine | 2ms | 2ms | +0ms |
| GWT attention | 5ms | 5ms (moved to bg) | +0ms |
| SONA patterns | 3ms | 3ms | +0ms |
| Meta curiosity | 1ms | 1ms | +0ms |
| **Temporal compare** | - | **3ms** | **+3ms** |
| **Attractor lookup** | - | **1ms** | **+1ms** |
| **Strange-loop lookup** | - | **1ms** | **+1ms** |
| Sort + truncate | 1ms | 1ms | +0ms |
| **Total** | **~45ms** | **~50ms** | **+5ms** |
Total read latency stays well under the 100ms budget. The scheduler actually reduces worst-case latency by removing SONA tick from the status handler.
### 10.2 Write Path (share_memory) Latency Impact
| Component | Current | With Midstream | Delta |
|-----------|---------|---------------|-------|
| PII + embed + witness + RVF | 80ms | 80ms | +0ms |
| DeltaStream push | 0.5ms | 0.5ms | +0ms |
| Meta-learning record | 0.5ms | 0.5ms | +0ms |
| **Temporal buffer append** | - | **0.1ms** | **+0.1ms** |
| **Attractor point push** | - | **0.1ms** | **+0.1ms** |
| **LTL state push** | - | **0.5ms** | **+0.5ms** |
| Graph + Firestore | 60ms | 60ms | +0ms |
| **Total** | **~141ms** | **~142ms** | **+0.7ms** |
### 10.3 Memory Footprint
| Component | Estimate | Notes |
|-----------|----------|-------|
| TemporalComparator | ~2MB | 1000 cached comparisons, 500-step sequences |
| RealtimeScheduler | <1MB | Priority queue of ~20 recurring tasks |
| AttractorAnalyzers (8 categories) | ~4MB | 8 * 1000-point trajectories * 128-dim |
| TemporalNeuralSolver | ~1MB | 10K trace buffer |
| StrangeLoop | <1MB | 3 meta-levels, bounded knowledge store |
| QuicFederationStats | <1KB | Statistics struct only (transport runs separately) |
| **Total** | **~8MB** | On top of existing ~10MB AGI state |
### 10.4 Background Task CPU Budget
The scheduler runs all background tasks on a single `tokio::spawn` loop. Worst-case CPU per cycle:
| Task | Frequency | CPU/cycle | Annual CPU-hours |
|------|-----------|-----------|-----------------|
| SONA tick | 10s | 2ms | 6.3h |
| GWT decay | 5s | 1ms | 6.3h |
| Delta compact | 60s | 10ms | 5.3h |
| LTL check | 30s | 5ms | 5.3h |
| Attractor analyze | 120s | 50ms | 13.1h |
| Strange-loop reflect | 300s | 100ms | 10.5h |
| **Total** | - | - | **46.8h/year** |
At Cloud Run pricing ($0.000024/vCPU-second), annual background cost is approximately $4.04.
## 11. Safety Considerations
### 11.1 strange-loop Self-Modification Bounds
The strange-loop crate's self-modification capability is the highest-risk component. The following safety envelope is enforced:
**Hard limits (cannot be overridden)**:
- Maximum meta-depth: 3 levels (prevents infinite recursion)
- `always_safe()` constraint: no modification can produce a state where scoring coefficients sum to > 1.0
- `eventually_terminates()` constraint: every `learn_at_level()` call must complete within 1000ms
**Soft limits (can be tuned via env vars)**:
- `STRANGE_LOOP_MAX_DEPTH` (default: 3, range: 1-5)
- `STRANGE_LOOP_MODIFICATION_RATE` (default: 1 per 300s, range: 1 per 60s to 1 per 3600s)
- `STRANGE_LOOP_CURIOSITY_MIN` (default: 0.01)
- `STRANGE_LOOP_CURIOSITY_MAX` (default: 0.20)
**Rollback mechanism**: If a self-modification causes any LTL property (Phase 9d) to fail in the next check cycle, the modification is automatically reverted and the strange-loop's modification capability is suspended for 1 hour (with WARN log).
### 11.2 LTL Property Definitions
The five core invariant properties are defined in Section 8 (Phase 9d). Additional properties can be added at runtime via the scheduler, but existing properties cannot be removed without a code change.
**Verification semantics**: Properties are checked over the *trace buffer* (last 10K state transitions), not over all-time history. This bounds verification time and avoids false positives from pre-midstream state.
### 11.3 Temporal Compare Privacy
DTW trajectory matching could theoretically leak information about a contributor's knowledge evolution pattern. Mitigation:
- Trajectory buffers are category-scoped, not contributor-scoped
- Embeddings in trajectories have already been DP-noised (if `RVF_DP_ENABLED=true`)
- The `find_similar_generic` API returns distance scores only, not raw trajectories
### 11.4 Scheduler Deadline Misses
If the scheduler misses a deadline (e.g., under high load), the task is still executed but:
- `missed_deadlines` counter is incremented (visible in `/v1/scheduler/stats`)
- If `missed_deadlines > 10` in any 60s window, emit WARN log
- If `missed_deadlines > 50` in any 60s window, disable lowest-priority tasks (Background(10)) for 5 minutes
## 12. Consequences
### Positive
- **Temporal pattern matching** enables discovery of memories with similar evolution trajectories, surfacing non-obvious relationships that cosine similarity alone misses
- **Deadline-aware scheduling** removes AGI maintenance work from the request hot path, reducing p99 read latency by ~5ms
- **Dynamical systems analysis** upgrades drift monitoring from simple CV thresholds to Lyapunov-exponent-based attractor classification, enabling early detection of chaotic knowledge domains
- **LTL verification** provides formal guarantees on system invariants, catching subtle bugs (dimension mismatches, witness chain gaps) that procedural checks might miss
- **Recursive meta-cognition** allows the brain server to self-tune exploration and scoring parameters based on accumulated evidence, reducing manual tuning burden
- **QUIC transport** (future) enables sub-millisecond brain-to-brain federation with 0-RTT resumption, a prerequisite for real-time multi-brain mesh
- **All features are independently feature-gated** with runtime env vars, zero risk to existing behavior when disabled
- **Total latency impact of +5ms on reads and +0.7ms on writes** stays well within the 100ms read budget
### Negative
- **Six additional crate dependencies** increase compile time by an estimated 30-45 seconds (quic-multistream alone adds ~20s due to quinn/rustls)
- **~8MB additional memory footprint** for in-memory trajectory buffers, analyzer state, and solver traces
- **Increased operational complexity**: six new env vars, six new endpoints, scheduler monitoring
- **strange-loop self-modification** introduces a novel failure mode: if safety constraints are misconfigured, the system could oscillate between parameter settings
- **LTL verification has false-positive risk** on edge cases where the trace buffer wraps around and loses historical context
### Risks
- **temporal-compare DTW** on high-dimensional (128-dim) sequences may be slower than expected if trajectory lengths grow beyond 100 steps; mitigation: cap max_sequence_length at 500
- **nanosecond-scheduler** is designed for sub-microsecond scheduling, but tokio's cooperative scheduling has ~1ms granularity; actual scheduling precision will be millisecond-level, not nanosecond
- **temporal-attractor-studio** Lyapunov exponent estimation requires sufficient trajectory data (minimum ~30 points per category); new categories will show `insufficient_data` until populated
- **temporal-neural-solver** LTL verification confidence degrades if state transitions are sparse; the 30s check interval may verify the same state repeatedly during low-traffic periods
- **strange-loop** meta-learning effectiveness depends on diverse input signals; if traffic is dominated by a single category, meta-knowledge will be narrowly scoped
- **quic-multistream** requires TLS certificates for peer authentication; certificate management adds operational burden for multi-brain deployments
- **Crate version stability**: all midstream crates are at `0.1.x`; API changes in minor versions could require brain server updates
## 13. Verification
### 13.1 Compilation
1. `cargo check -p mcp-brain-server` compiles with zero errors after adding all six dependencies
2. `cargo check -p mcp-brain-server --features quic-federation` compiles with quic-multistream enabled
### 13.2 Existing Test Regression
3. All Phase 1-8 tests continue to pass with all midstream flags disabled (default)
4. `/v1/status` returns all existing fields unchanged when midstream flags are off
### 13.3 Feature Flag Isolation
5. Enabling any single midstream flag does not affect behavior of other subsystems
6. Disabling a midstream flag mid-operation (via restart) gracefully drops in-memory state without errors
### 13.4 New Endpoint Validation
7. `GET /v1/temporal/trajectories?memory_id=<valid-uuid>` returns valid JSON with `similar_trajectories` array
8. `GET /v1/scheduler/stats` returns valid JSON with `total_scheduled >= 0`
9. `GET /v1/attractor` returns valid JSON with per-category `attractor_type` field
10. `GET /v1/invariants` returns valid JSON with `properties_defined >= 5`
11. `GET /v1/meta/strange-loop` returns valid JSON with `meta_depth <= 3`
12. `GET /v1/federation/stats` returns valid JSON with `status: "standby"` when no peers connected
### 13.5 Scoring Pipeline Integrity
13. With all midstream flags enabled, total midstream score contribution never exceeds 0.13 for any single memory
14. Search results with midstream enabled are a superset-reordering of results without midstream (no memories added or removed, only rank changes)
### 13.6 Safety Constraints
15. `strange-loop` with `max_meta_depth: 3` never recurses beyond level 3 (test with artificial depth-forcing input)
16. Self-modification that would set curiosity weight outside [0.01, 0.20] is rejected by safety constraint
17. LTL violation triggers WARN log but does not block the violating operation
18. Scheduler deadline miss increments counter without dropping the task
### 13.7 Performance
19. `search_memories` with all midstream flags enabled completes in < 100ms for 238 memories (p99)
20. `share_memory` with all midstream flags enabled completes in < 200ms (p99)
21. Background scheduler CPU usage stays under 1% of a single vCPU at steady state
22. Memory footprint with all midstream state initialized stays under 20MB total (existing + midstream)
### 13.8 Load Test
23. 40-concurrent search requests with all midstream flags enabled: p90 < 200ms, p99 < 300ms
24. No scheduler deadline misses under 40-concurrent sustained load for 60 seconds
## 14. Migration Notes
### 14.1 Backward Compatibility
All existing API contracts are unchanged. New endpoints are additive. Midstream scoring signals are additive and bounded. No breaking changes to any existing client.
### 14.2 Firestore Schema
No Firestore schema changes required. All midstream state is in-memory only. Persistence of midstream state (attractor histories, LTL traces, meta-knowledge) is a future consideration for a follow-up ADR.
### 14.3 Docker / Cloud Run
The Dockerfile at `crates/mcp-brain-server/Dockerfile` needs no changes for Phases 9a-9e (pure Rust crates). Phase 9f (quic-multistream) requires exposing an additional UDP port in the Cloud Run service configuration:
```yaml
# cloudbuild.yaml addition for Phase 9f only
ports:
- containerPort: 8080 # existing HTTP
- containerPort: 4433 # QUIC federation (UDP)
protocol: UDP
```
Note: Cloud Run currently has limited UDP support. Phase 9f may require migration to GKE or a hybrid deployment model.

View file

@ -0,0 +1,378 @@
# ADR-078: npx ruvector Midstream & Brain AGI Integration
**Status**: Proposed
**Date**: 2026-03-03
**Authors**: RuVector Team
**Deciders**: ruv
**Supersedes**: N/A
**Related**: ADR-070 (npx ruvector Unified Integration), ADR-076 (AGI Capability Wiring), ADR-077 (Midstream Brain Integration)
## 1. Context
The mcp-brain-server backend at π.ruv.io now has 8 AGI subsystems deployed and operational (ADR-076, ADR-077):
- **SONA** — 3-tier hierarchical learning engine
- **GWT** — Global Workspace Theory attention competition
- **Temporal Delta Tracking** — Knowledge evolution velocity
- **Meta-Learning Exploration** — Thompson Sampling with curiosity/regret
- **Nanosecond Scheduler** — Background task scheduling
- **Temporal Attractor Studio** — Lyapunov exponent analysis for embedding stability
- **Temporal Neural Solver** — Certified temporal predictions with solver gates
- **Strange Loop** — Recursive meta-cognitive reasoning
The backend exposes 5 diagnostic endpoints (`/v1/status`, `/v1/sona/stats`, `/v1/temporal`, `/v1/explore`, `/v1/midstream`) and returns AGI-enriched search results. However, **none of these capabilities are exposed through the `npx ruvector` CLI (167 commands) or the MCP server (118 tools)**.
The existing brain CLI commands (13) and MCP tools (11) were implemented against the Phase 1-7 API surface. They don't surface AGI diagnostics, midstream analytics, or the enriched scoring pipeline metadata.
### Current State
| Layer | Brain Commands | AGI/Midstream Commands |
|-------|---------------|----------------------|
| Backend (mcp-brain-server) | 33 REST endpoints | 5 AGI endpoints, 4 midstream subsystems |
| MCP Client (mcp-brain) | 20 MCP tools | 0 |
| npm CLI (npx ruvector) | 13 brain subcommands | 0 |
| npm MCP (mcp-server.js) | 11 brain MCP tools | 0 |
### Gap
Users cannot:
1. View SONA learning patterns, trajectories, or background tick state
2. Monitor temporal delta velocity or trend
3. Inspect meta-learning regret, curiosity scores, or plateau status
4. View midstream scheduler metrics, attractor analysis, or strange-loop version
5. See AGI scoring layer contributions in search results
6. Toggle midstream feature flags without redeploying
## 2. Decision
Extend `npx ruvector` with 2 new CLI command groups and 12 new MCP tools that surface all AGI and midstream capabilities from the backend.
### 2.1 New CLI Commands
#### `ruvector brain agi` Subcommand Group (6 commands)
```
ruvector brain agi status # Combined AGI + midstream diagnostics
ruvector brain agi sona # SONA patterns, trajectories, background ticks
ruvector brain agi temporal # Temporal delta velocity, trend, total deltas
ruvector brain agi explore # Meta-learning curiosity, regret, plateau, Pareto
ruvector brain agi midstream # Scheduler ticks, attractor categories, solver, strange-loop
ruvector brain agi flags # List current feature flag state from /v1/status
```
All commands support `--json` for machine-readable output and `--url`/`--key` for backend override.
**Example output:**
```
$ npx ruvector brain agi status
┌─────────────────────────────────────┐
│ π.ruv.io AGI Diagnostics │
├─────────────────────────────────────┤
│ SONA │
│ Patterns: 12 Trajectories: 45 │
│ Background ticks: 3 │
│ │
│ GWT Attention │
│ Workspace load: 0.42 │
│ Avg salience: 0.31 │
│ │
│ Temporal │
│ Total deltas: 237 │
│ Velocity: 14.0/hr │
│ Trend: growing │
│ │
│ Meta-Learning │
│ Avg regret: 0.023 │
│ Plateau: learning │
│ │
│ Midstream │
│ Scheduler ticks: 1,204 │
│ Attractor categories: 5 │
│ Strange-loop: v0.3.0 │
└─────────────────────────────────────┘
```
#### `ruvector midstream` Command Group (4 commands)
```
ruvector midstream status # Midstream platform overview
ruvector midstream attractor [cat] # Lyapunov analysis per category
ruvector midstream scheduler # Scheduler metrics (ticks, tasks/sec)
ruvector midstream benchmark # Run latency benchmark against backend
```
### 2.2 New MCP Tools
| Tool Name | Endpoint | Description |
|-----------|----------|-------------|
| `brain_agi_status` | GET /v1/status | Combined AGI diagnostics (SONA + GWT + temporal + meta + midstream fields) |
| `brain_sona_stats` | GET /v1/sona/stats | SONA patterns, trajectories, background ticks |
| `brain_temporal` | GET /v1/temporal | Temporal delta velocity, trend, total deltas |
| `brain_explore` | GET /v1/explore | Meta-learning curiosity, regret, plateau, Pareto |
| `brain_midstream` | GET /v1/midstream | Midstream scheduler, attractor, solver, strange-loop |
| `brain_flags` | GET /v1/status | Extract and display feature flag state |
| `midstream_status` | GET /v1/midstream | Full midstream platform diagnostics |
| `midstream_attractor` | GET /v1/midstream | Attractor categories with Lyapunov exponents |
| `midstream_scheduler` | GET /v1/midstream | Nanosecond scheduler performance metrics |
| `midstream_benchmark` | Multi-endpoint | Run sequential + concurrent latency benchmark |
| `midstream_search` | GET /v1/memories/search | Search with midstream scoring metadata in response |
| `midstream_health` | GET /v1/health + /v1/midstream | Combined health + midstream subsystem check |
### 2.3 Enhanced Brain Commands
Existing commands get optional AGI metadata:
| Command | Enhancement |
|---------|------------|
| `brain search` | Add `--verbose` flag to show per-result AGI scoring breakdown (SONA pattern boost, GWT attention winner, meta curiosity, attractor stability, strange-loop bonus) |
| `brain status` | Include AGI and midstream fields in default output (already returned by backend, just not displayed) |
| `brain share` | Show attractor update status when midstream is enabled |
### 2.4 Response Schema Extension
The backend already returns AGI/midstream fields in `/v1/status`. The CLI and MCP server need to parse and display them:
```json
{
"sona_patterns": 12,
"sona_trajectories": 45,
"gwt_workspace_load": 0.42,
"gwt_avg_salience": 0.31,
"knowledge_velocity": 14.0,
"temporal_deltas": 237,
"meta_avg_regret": 0.023,
"meta_plateau_status": "learning",
"midstream_scheduler_ticks": 1204,
"midstream_attractor_categories": 5,
"midstream_strange_loop_version": "0.3.0"
}
```
## 3. Implementation Plan
### Phase 1: CLI `brain agi` Subcommands (cli.js)
**File**: `npm/packages/ruvector/bin/cli.js`
1. Add `agi` subcommand group under existing `brainCmd`:
```javascript
const agiCmd = brainCmd.command('agi')
.description('AGI subsystem diagnostics — SONA, GWT, temporal, meta-learning, midstream');
agiCmd.command('status')
.description('Combined AGI + midstream diagnostics from π.ruv.io')
.option('--json', 'Output as JSON')
.action(async (opts) => {
const piBrain = await requirePiBrain();
const config = getBrainConfig(opts);
const client = new piBrain.PiBrainClient(config);
const status = await client.status();
// Extract and format AGI fields
});
```
2. Add individual `sona`, `temporal`, `explore`, `midstream`, `flags` subcommands following same pattern.
3. Each command calls the corresponding backend endpoint via `PiBrainClient` and formats output with chalk.
**Estimated changes**: ~200 lines added to cli.js
### Phase 2: CLI `midstream` Command Group (cli.js)
**File**: `npm/packages/ruvector/bin/cli.js`
1. Add new top-level command group:
```javascript
const midstreamCmd = program.command('midstream')
.description('Midstream real-time streaming analysis platform');
```
2. Add `status`, `attractor`, `scheduler`, `benchmark` subcommands.
3. The `benchmark` command runs sequential + concurrent latency tests:
- 3 sequential requests to each endpoint (health, status, search, write)
- 20 concurrent search requests
- Reports p50/p90/p99 latencies
**Estimated changes**: ~150 lines added to cli.js
### Phase 3: MCP Tools (mcp-server.js)
**File**: `npm/packages/ruvector/bin/mcp-server.js`
1. Add 12 new tool definitions to the tools array:
```javascript
{
name: 'brain_agi_status',
description: 'Combined AGI subsystem diagnostics from the shared brain',
inputSchema: {
type: 'object',
properties: {},
}
},
// ... 11 more tools
```
2. Add handler cases:
```javascript
case 'brain_agi_status':
case 'brain_sona_stats':
case 'brain_temporal':
case 'brain_explore':
case 'brain_midstream':
case 'brain_flags': {
const { PiBrainClient } = require('@ruvector/pi-brain');
const client = new PiBrainClient({ url, key });
const endpointMap = {
brain_agi_status: 'status',
brain_sona_stats: 'sona/stats',
brain_temporal: 'temporal',
brain_explore: 'explore',
brain_midstream: 'midstream',
brain_flags: 'status',
};
const subCmd = endpointMap[name];
const result = await client.raw(`/v1/${subCmd}`);
// For brain_flags, extract only flag-related fields
// For brain_agi_status, extract AGI fields
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
```
**Estimated changes**: ~250 lines added to mcp-server.js
### Phase 4: PiBrainClient Extension (@ruvector/pi-brain)
**File**: `npm/packages/pi-brain/` (or inline in cli.js if pi-brain not available)
Add methods:
- `sonaStats()` — GET /v1/sona/stats
- `temporal()` — GET /v1/temporal
- `explore()` — GET /v1/explore
- `midstream()` — GET /v1/midstream
- `raw(path)` — GET any /v1/* endpoint (generic)
If `@ruvector/pi-brain` is not yet published, implement these as inline fetch calls in cli.js using the existing `getBrainConfig()` pattern.
### Phase 5: Enhanced `brain search --verbose`
**File**: `npm/packages/ruvector/bin/cli.js`
Add `--verbose` flag to `brain search` that displays per-result metadata when available. The backend already includes `quality_score`, `witness_hash`, etc. in search results. The verbose mode would format these prominently:
```
$ npx ruvector brain search "neural embeddings" --verbose
1. Neural Embedding Patterns (pattern)
Quality: 0.89 | Votes: 12↑ 1↓ | Witness: 3a7f...
Contributor: anon-abc123 | Created: 2026-03-01
Tags: neural, embedding, rust
```
### Phase 6: Tests
**File**: `npm/packages/ruvector/test/cli-commands.js`
Add tests for new commands:
- `brain agi status --help` — verify command exists
- `brain agi sona --help` — verify subcommand exists
- `midstream status --help` — verify new command group
- `midstream benchmark --help` — verify benchmark command
**File**: `npm/packages/ruvector/test/integration.js`
Add integration test: verify new MCP tools appear in tool list.
**Estimated**: 12-15 new test cases
## 4. File Summary
| File | Action | Phase | Est. Lines |
|------|--------|-------|-----------|
| `npm/packages/ruvector/bin/cli.js` | Add `brain agi` + `midstream` commands | 1,2,5 | +400 |
| `npm/packages/ruvector/bin/mcp-server.js` | Add 12 MCP tools | 3 | +250 |
| `npm/packages/pi-brain/src/client.ts` | Add AGI/midstream methods | 4 | +60 |
| `npm/packages/ruvector/test/cli-commands.js` | New command tests | 6 | +50 |
| `npm/packages/ruvector/test/integration.js` | MCP tool integration tests | 6 | +30 |
| `npm/packages/ruvector/package.json` | Version bump 0.2.3 → 0.2.4 | 3 | +1 |
## 5. API Surface After Integration
| Layer | Before | After | Delta |
|-------|--------|-------|-------|
| CLI Commands | 167 | 177 | +10 |
| CLI Groups | 12 | 13 | +1 (`midstream`) |
| Brain Subcommands | 13 | 19 | +6 (`agi` group) |
| MCP Tools | 118 | 130 | +12 |
| Brain MCP Tools | 11 | 17 | +6 |
## 6. Backend Endpoints Consumed
All endpoints already exist and are deployed (revision ruvbrain-00071-wp7):
| Endpoint | Auth | New Consumers |
|----------|------|--------------|
| GET /v1/status | Yes | `brain agi status`, `brain agi flags`, `brain_agi_status`, `brain_flags` |
| GET /v1/sona/stats | Yes | `brain agi sona`, `brain_sona_stats` |
| GET /v1/temporal | Yes | `brain agi temporal`, `brain_temporal` |
| GET /v1/explore | Yes | `brain agi explore`, `brain_explore` |
| GET /v1/midstream | Yes | `brain agi midstream`, `midstream status`, `brain_midstream`, `midstream_status` |
| GET /v1/health | No | `midstream benchmark`, `midstream_health` |
| GET /v1/memories/search | Yes | `midstream_search` (with scoring metadata) |
No backend changes required — all endpoints are live and returning the expected JSON schemas.
## 7. Feature Flags
No new environment variables. The CLI/MCP tools read from the existing backend responses. Feature flag state is reported by the backend in `/v1/status` response.
For local development/testing, existing env vars apply:
- `BRAIN_URL` — Override backend URL (default: π.ruv.io Cloud Run URL)
- `BRAIN_API_KEY` — API key for authentication
## 8. Backward Compatibility
- All new commands are additive — no existing commands modified
- `brain status` enhanced output is backward-compatible (new fields appended)
- `brain search --verbose` is opt-in (default output unchanged)
- MCP tools follow existing naming convention (`brain_*`, `midstream_*`)
- `@ruvector/pi-brain` methods are additive (existing API unchanged)
- Version bump is minor (0.2.3 → 0.2.4) — no breaking changes
## 9. Consequences
### Positive
- Users gain full visibility into all 8 AGI subsystems via CLI and MCP
- Claude Code agents can inspect brain learning state (SONA patterns, meta-learning regret) to make informed decisions
- Midstream platform metrics enable monitoring and alerting on knowledge evolution
- `midstream benchmark` provides a standardized latency test for the brain backend
- Feature flag visibility enables debugging deployment issues
### Negative
- cli.js grows by ~400 lines (8,500 → 8,900) — still within single-file budget
- mcp-server.js grows by ~250 lines (3,500 → 3,750) — acceptable
- 12 additional MCP tools increase tool list size (agents may need to filter)
- `@ruvector/pi-brain` gains a dependency on 5 new endpoint paths
### Risks
- Backend `/v1/status` response schema may evolve — CLI should handle missing fields gracefully
- Midstream feature flags defaulting to `false` means `brain agi midstream` shows zeros until enabled
- `midstream benchmark` generates real traffic — should include rate limiting awareness
## 10. Verification
1. `npm test` in `npm/packages/ruvector/` — 55 existing + 12-15 new tests pass
2. `npx ruvector brain agi status --json` returns valid JSON with AGI fields
3. `npx ruvector midstream status --json` returns midstream diagnostics
4. `npx ruvector midstream benchmark` completes within 30s and reports latencies
5. MCP server lists 130 tools (was 118)
6. Claude Code can call `brain_agi_status` tool and receive formatted response
7. `npx ruvector brain search "test" --verbose` shows enhanced output
8. All commands work with `--url` and `--key` overrides

View file

@ -0,0 +1,101 @@
# ADR-079: SQL Audit Script Hardening & Bug Fixes
**Status:** Accepted
**Date:** 2026-03-03
**Author:** ruvnet
## Context
The ruvector independent audit verification script (`sql-audit.sql`) v2 contained 12 bugs ranging from syntax errors that prevent execution to logic errors that produce misleading results. The script tests 13 advertised ruvector extension features against actual behavior — correctness of the audit tool itself is critical for trust.
## Issues Found (v2 -> v3)
### Critical (5)
| # | Issue | Impact |
|---|-------|--------|
| 1 | **Dollar quoting broken** — All DO blocks use single `$` instead of `$$`. PostgreSQL requires `$$` or `$tag$` for dollar-quoted string literals. | Every DO block is a syntax error — script cannot run at all |
| 2 | **Hardcoded node IDs in shortest_path** (Section 4d) — Uses literal `1, 3` but auto-generated IDs vary by database state. IDs from Section 4b's DO block are local variables, unreachable in 4d. | Shortest path test fails on any non-empty database |
| 3 | **Section 5b bare SELECTs** — `ruvector_insert_triple()` calls have no DO/EXCEPTION wrapper. If the function doesn't exist, the script aborts entirely. | Breaks fault-tolerance guarantee |
| 4 | **dblink connection string unquoted** — `'dbname=' \|\| current_database()` is vulnerable to breakage with special characters in database/user names. | Persistence test fails on databases with spaces/special chars |
| 5 | **GUC `hnsw.ef_search` unguarded** — `SET hnsw.ef_search = 200` throws an error if ruvector doesn't register this custom GUC parameter. | HNSW test section aborts |
### Important (4)
| # | Issue | Impact |
|---|-------|--------|
| 6 | **Section 11 inconsistent filtering** — Uses `pg_namespace` join instead of `pg_depend + pg_extension`, unlike Section 0. May report non-ruvector functions as ruvector features. | False positives in bonus capabilities check |
| 7 | **Session GUCs not restored** — `hnsw.ef_search`, `client_min_messages` never reset. | Affects user's psql session after audit |
| 8 | **Section 5b results not validated** — Triple INSERT output printed but never checked PASS/FAIL. | Misleading — user sees output but no verdict |
| 9 | **Section 4c graph_stats outside exception block** — Bare SELECT aborts script if graph creation failed in 4b. | Breaks fault tolerance |
### Minor (3)
| # | Issue | Impact |
|---|-------|--------|
| 10 | `\timing` scope inconsistent across sections | Timing data missing for Sections 3-10 |
| 11 | Cypher test (4e) not programmatically validated | Relies on human to spot self-reference bug |
| 12 | `enable_indexscan = off` not wrapped in savepoint | Script interruption leaves index scans disabled |
## Decision
Create v3 (`scripts/sql-audit-v3.sql`) with all 12 fixes applied:
1. **Dollar quoting** — All DO blocks use `$$` or named tags (`$audit_NNN$`, `$graph_create$`, etc.)
2. **Node ID passing** — Temp table `_audit_graph_ids` bridges DO blocks; shortest_path reads from it
3. **Full fault tolerance** — Every external call wrapped in DO/EXCEPTION; no bare SELECTs for ruvector functions
4. **Safe dblink** — `format('dbname=%L user=%L', current_database(), current_user)` with proper quoting
5. **GUC guards** — `SET LOCAL hnsw.ef_search` inside nested BEGIN/EXCEPTION
6. **Consistent filtering** — All Section 11 queries use `pg_depend + pg_extension` join
7. **Session restore** — `RESET client_min_messages` at cleanup; `SET LOCAL` for all temporary GUCs
8. **Programmatic verdicts** — All sections emit PASS/FAIL/ERROR via RAISE NOTICE with value checks
9. **Savepoint safety** — `SET LOCAL enable_indexscan` scoped to DO block transaction
## Consequences
- Audit script is now fully executable on any PostgreSQL 14-17 installation
- No section can abort the rest — all wrapped in exception handlers
- Results are machine-parseable (grep for `PASS:` / `FAIL:` / `ERROR:`)
- Session state is clean after script completes
## Known ruvector Issues Discovered by Audit
| # | Issue | Status | Fix |
|---|-------|--------|-----|
| 1 | Cypher MATCH self-reference bug (`a.id == b.id`) | **Fixed (v0.3.1)** | Rewrote `match_pattern()` in `executor.rs` to properly traverse edges, reject self-references when variables differ, and generate per-edge binding rows |
| 2 | Graph/RDF persistence failure (in-memory only) | **Fixed (v0.3.1)** | Added PostgreSQL backing tables (`_ruvector_graphs`, `_ruvector_nodes`, `_ruvector_edges`, `_ruvector_rdf_stores`, `_ruvector_triples`) with auto-load on cache miss |
| 3 | HNSW index scan returns 0 rows despite correct query planning | Open | v0.1.0 SQL schema issue — requires investigation of index AM registration |
| 4 | Self-healing, multi-tenancy, hybrid search "not registered" | **Fixed (v0.3.1)** | 46 missing `CREATE FUNCTION` statements added to `ruvector--0.3.0.sql`: GNN (5), healing (17), tenancy (17), hybrid (7). Modules were always compiled but SQL schema lacked function registrations. All 46 verified in Docker container. |
| 5 | SONA apply panics on non-256-dim input | **Fixed (v0.3.1)** | Dynamic dimension detection with per-dim engine caching and `catch_unwind` panic guard |
## Related Changes (v0.3.1)
### Rust Source Fixes
- `crates/ruvector-postgres/src/graph/cypher/executor.rs` — Cypher self-reference fix
- `crates/ruvector-postgres/src/graph/mod.rs` — Graph persistence tables + `use pgrx::JsonB` + `get_by_name::<T, _>()` fix
- `crates/ruvector-postgres/src/graph/sparql/mod.rs` — RDF persistence tables + `get_by_name::<T, _>()` fix
- `crates/ruvector-postgres/src/graph/operators.rs` — Persist calls after node/edge/triple inserts
- `crates/ruvector-postgres/src/sona/mod.rs` — Dynamic dimension engine cache (`dim as usize` cast)
- `crates/ruvector-postgres/src/sona/operators.rs` — Dimension detection + `catch_unwind` panic guard
### SQL Schema
- `crates/ruvector-postgres/sql/ruvector--0.3.0.sql` — Added 46 `CREATE FUNCTION` statements for GNN (5), healing (17), tenancy (17), hybrid (7). Total extension functions: **190**
### Docker
- `crates/ruvector-postgres/Dockerfile` — Updated labels, features, SQL copy for v0.3.1
- `crates/ruvector-postgres/Dockerfile.prebuilt` — New slim image using pre-compiled artifacts (~12s build)
- `crates/ruvector-postgres/docker/Dockerfile` — Updated Rust 1.85, features, labels
- `crates/ruvector-postgres/docker/docker-compose.yml` — Updated Rust version to 1.85
- **Published**: `docker.io/ruvnet/ruvector-postgres:0.3.1` and `:latest` (sha256:6d2f28ed5efd, 151 MB)
### Verification Summary
All 46 new functions verified in Docker container (`ruvnet/ruvector-postgres:0.3.1`):
| Module | Functions | Status |
|--------|-----------|--------|
| GNN | `ruvector_gcn_forward`, `ruvector_gnn_aggregate`, `ruvector_message_pass`, `ruvector_graphsage_forward`, `ruvector_gnn_batch_forward` | 5/5 PASS |
| Self-Healing | `ruvector_health_status`, `ruvector_is_healthy`, `ruvector_system_metrics`, `ruvector_healing_history`, `ruvector_healing_history_since`, `ruvector_healing_history_for_strategy`, `ruvector_healing_trigger`, `ruvector_healing_execute`, `ruvector_healing_configure`, `ruvector_healing_get_config`, `ruvector_healing_enable`, `ruvector_healing_strategies`, `ruvector_healing_effectiveness`, `ruvector_healing_stats`, `ruvector_healing_thresholds`, `ruvector_healing_set_thresholds`, `ruvector_healing_problem_types` | 17/17 PASS |
| Multi-Tenancy | `ruvector_tenant_create`, `ruvector_tenant_set`, `ruvector_tenant_stats`, `ruvector_tenant_quota_check`, `ruvector_tenant_suspend`, `ruvector_tenant_resume`, `ruvector_tenant_delete`, `ruvector_tenants`, `ruvector_enable_tenant_rls`, `ruvector_tenant_migrate`, `ruvector_tenant_migration_status`, `ruvector_tenant_isolate`, `ruvector_tenant_set_policy`, `ruvector_tenant_update_quota`, `ruvector_generate_rls_sql`, `ruvector_generate_tenant_column_sql`, `ruvector_generate_roles_sql` | 17/17 PASS |
| Hybrid Search | `ruvector_register_hybrid`, `ruvector_hybrid_update_stats`, `ruvector_hybrid_configure`, `ruvector_hybrid_search`, `ruvector_hybrid_stats`, `ruvector_hybrid_score`, `ruvector_hybrid_list` | 7/7 PASS |
| SONA (prev fix) | `ruvector_sona_apply` with 3-dim and 5-dim inputs | 2/2 PASS |

View file

@ -621,7 +621,7 @@ dependencies = [
[[package]]
name = "ruvector-exotic-wasm"
version = "0.1.29"
version = "2.0.5"
dependencies = [
"getrandom 0.2.16",
"getrandom 0.3.4",

View file

@ -0,0 +1,322 @@
import { useState, useEffect } from 'react';
import { Card, CardBody, Progress } from '@heroui/react';
import { motion } from 'framer-motion';
import {
Brain,
Link2,
Zap,
Coins,
Clock,
BookOpen,
TrendingUp,
Activity,
} from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
// Simulated brain integration state (would come from real WASM/relay in production)
interface BrainState {
connectionHealth: 'connected' | 'degraded' | 'disconnected';
relayLatency: number;
operationsToday: number;
ruvEarnedToday: number;
halvingEpoch: number;
epochProgress: number;
topCategories: { name: string; queries: number; color: string }[];
brainUptime: number;
totalKnowledgeEntries: number;
}
function useBrainState(): BrainState {
const { isRelayConnected, stats, credits } = useNetworkStore();
const [brainState, setBrainState] = useState<BrainState>({
connectionHealth: 'disconnected',
relayLatency: 0,
operationsToday: 0,
ruvEarnedToday: 0,
halvingEpoch: 1,
epochProgress: 0.37,
topCategories: [
{ name: 'Code Patterns', queries: 1842, color: 'sky' },
{ name: 'Architecture', queries: 1203, color: 'violet' },
{ name: 'Security', queries: 891, color: 'amber' },
{ name: 'Performance', queries: 654, color: 'emerald' },
{ name: 'DevOps', queries: 412, color: 'cyan' },
],
brainUptime: 0,
totalKnowledgeEntries: 0,
});
useEffect(() => {
const interval = setInterval(() => {
setBrainState((prev) => {
const health = isRelayConnected
? stats.latency < 100
? 'connected'
: 'degraded'
: 'disconnected';
return {
...prev,
connectionHealth: health,
relayLatency: stats.latency,
operationsToday: Math.floor(stats.tasksCompleted * 2.3),
ruvEarnedToday: Math.round(credits.earned * 0.4 * 100) / 100,
brainUptime: prev.brainUptime + 1,
totalKnowledgeEntries: 24_831 + Math.floor(prev.brainUptime / 5),
topCategories: prev.topCategories.map((cat) => ({
...cat,
queries: cat.queries + Math.floor(Math.random() * 3),
})),
};
});
}, 2000);
return () => clearInterval(interval);
}, [isRelayConnected, stats.latency, stats.tasksCompleted, credits.earned]);
return brainState;
}
const healthConfig = {
connected: {
label: 'Healthy',
color: 'emerald',
bgClass: 'bg-emerald-500/10 border-emerald-500/30',
textClass: 'text-emerald-400',
dotClass: 'bg-emerald-400',
},
degraded: {
label: 'Degraded',
color: 'amber',
bgClass: 'bg-amber-500/10 border-amber-500/30',
textClass: 'text-amber-400',
dotClass: 'bg-amber-400',
},
disconnected: {
label: 'Disconnected',
color: 'red',
bgClass: 'bg-red-500/10 border-red-500/30',
textClass: 'text-red-400',
dotClass: 'bg-red-400',
},
};
const categoryColorMap: Record<string, string> = {
sky: 'bg-sky-500/20 text-sky-400 border-sky-500/30',
violet: 'bg-violet-500/20 text-violet-400 border-violet-500/30',
amber: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
emerald: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
cyan: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
};
export function BrainStatus() {
const brain = useBrainState();
const health = healthConfig[brain.connectionHealth];
const maxQueries = Math.max(...brain.topCategories.map((c) => c.queries));
return (
<div className="space-y-6">
{/* Connection Health Banner */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-4 rounded-lg border flex items-center justify-between ${health.bgClass}`}
>
<div className="flex items-center gap-3">
<motion.div
className={`w-3 h-3 rounded-full ${health.dotClass}`}
animate={
brain.connectionHealth === 'connected'
? { scale: [1, 1.3, 1], opacity: [1, 0.7, 1] }
: {}
}
transition={{ duration: 2, repeat: Infinity }}
/>
<Brain className={health.textClass} size={20} />
<div>
<span className={`font-medium ${health.textClass}`}>
Brain Link: {health.label}
</span>
<p className="text-xs text-zinc-500 mt-0.5">
{brain.connectionHealth === 'connected'
? `Relay latency: ${brain.relayLatency.toFixed(0)}ms`
: brain.connectionHealth === 'degraded'
? `High latency: ${brain.relayLatency.toFixed(0)}ms`
: 'Enable contribution to connect'}
</p>
</div>
</div>
{brain.connectionHealth !== 'disconnected' && (
<div className="flex items-center gap-1 text-xs text-zinc-500">
<Link2 size={12} />
<span>relay &rarr; brain</span>
</div>
)}
</motion.div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="bg-gradient-to-br from-sky-500/20 to-sky-600/10 border border-sky-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Activity className="text-sky-400" size={22} />
<span className="text-xs text-sky-400/70">Today</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{brain.operationsToday.toLocaleString()}
</p>
<p className="text-sm text-sky-400 mt-1">Brain Operations</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 border border-emerald-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Coins className="text-emerald-400" size={22} />
<span className="text-xs text-emerald-400/70">Earned</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{brain.ruvEarnedToday.toFixed(2)}
</p>
<p className="text-sm text-emerald-400 mt-1">rUv from Brain</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-gradient-to-br from-violet-500/20 to-violet-600/10 border border-violet-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Clock className="text-violet-400" size={22} />
<span className="text-xs text-violet-400/70">Epoch</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{brain.halvingEpoch}
</p>
<p className="text-sm text-violet-400 mt-1">Halving Epoch</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="bg-gradient-to-br from-cyan-500/20 to-cyan-600/10 border border-cyan-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<BookOpen className="text-cyan-400" size={22} />
<span className="text-xs text-cyan-400/70">Total</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{(brain.totalKnowledgeEntries / 1000).toFixed(1)}k
</p>
<p className="text-sm text-cyan-400 mt-1">Knowledge Entries</p>
</CardBody>
</Card>
</motion.div>
</div>
{/* Halving Epoch Progress */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Zap className="text-violet-400" size={18} />
Halving Epoch Progress
</h3>
<span className="text-sm text-zinc-400">
Epoch {brain.halvingEpoch} of 20
</span>
</div>
<div className="mb-3">
<div className="flex justify-between text-sm mb-2">
<span className="text-zinc-400">Progress to next halving</span>
<span className="text-violet-400">
{(brain.epochProgress * 100).toFixed(0)}%
</span>
</div>
<Progress
value={brain.epochProgress * 100}
maxValue={100}
classNames={{
indicator: 'bg-gradient-to-r from-violet-500 to-pink-500',
track: 'bg-zinc-800',
}}
/>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<div className="text-center p-3 rounded-lg bg-zinc-800/50">
<p className="text-lg font-bold text-white">1.0x</p>
<p className="text-xs text-zinc-400">Current Rate</p>
</div>
<div className="text-center p-3 rounded-lg bg-zinc-800/50">
<p className="text-lg font-bold text-white">0.5x</p>
<p className="text-xs text-zinc-400">Next Epoch Rate</p>
</div>
<div className="text-center p-3 rounded-lg bg-zinc-800/50">
<p className="text-lg font-bold text-white">10B</p>
<p className="text-xs text-zinc-400">Max Supply rUv</p>
</div>
</div>
</motion.div>
{/* Top Knowledge Categories */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="text-sky-400" size={18} />
Top Knowledge Categories
</h3>
<div className="space-y-3">
{brain.topCategories.map((cat, idx) => (
<div key={cat.name} className="flex items-center gap-3">
<span className="text-xs text-zinc-500 w-4">{idx + 1}</span>
<div className="flex-1">
<div className="flex justify-between text-sm mb-1">
<span className="text-zinc-300">{cat.name}</span>
<span
className={`text-xs px-2 py-0.5 rounded-full border ${categoryColorMap[cat.color]}`}
>
{cat.queries.toLocaleString()} queries
</span>
</div>
<div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-sky-500 to-violet-500"
initial={{ width: 0 }}
animate={{ width: `${(cat.queries / maxQueries) * 100}%` }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
/>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,551 @@
import { useState, useEffect, useMemo } from 'react';
import { Card, CardBody, Progress } from '@heroui/react';
import { motion } from 'framer-motion';
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
AreaChart,
Area,
} from 'recharts';
import {
TrendingUp,
Users,
Coins,
Shield,
Award,
Gauge,
CircleDollarSign,
Gift,
} from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
// Economics model types
interface ReputationTier {
name: string;
minScore: number;
color: string;
count: number;
benefits: string;
}
interface SupplyStats {
totalMinted: number;
maxSupply: number;
contributorPool: number;
treasury: number;
protocolFund: number;
founderPool: number;
}
interface LeaderboardEntry {
rank: number;
name: string;
ruvEarned: number;
tasks: number;
tier: string;
}
interface EconomicsState {
contributionMultiplier: number;
multiplierCurve: { x: number; y: number }[];
reputationTiers: ReputationTier[];
supply: SupplyStats;
freeReadsToday: number;
freeReadsLimit: number;
leaderboard: LeaderboardEntry[];
velocity: number;
utilization: number;
stability: number;
}
function useEconomicsState(): EconomicsState {
const { stats, credits } = useNetworkStore();
const [economics, setEconomics] = useState<EconomicsState>(() => ({
contributionMultiplier: 1.0,
multiplierCurve: Array.from({ length: 20 }, (_, i) => ({
x: i * 5,
y: Math.min(3.0, 1.0 + Math.log1p(i * 5 * 0.02) * 1.2),
})),
reputationTiers: [
{ name: 'Observer', minScore: 0, color: '#71717a', count: 2841, benefits: 'Free reads only' },
{ name: 'Contributor', minScore: 0.3, color: '#38bdf8', count: 1592, benefits: '+1.2x multiplier' },
{ name: 'Builder', minScore: 0.5, color: '#a78bfa', count: 823, benefits: '+1.8x, priority queue' },
{ name: 'Architect', minScore: 0.7, color: '#fbbf24', count: 241, benefits: '+2.5x, governance' },
{ name: 'Guardian', minScore: 0.9, color: '#34d399', count: 47, benefits: '+3.0x, all access' },
],
supply: {
totalMinted: 847_293_100,
maxSupply: 10_000_000_000,
contributorPool: 593_105_170,
treasury: 127_093_965,
protocolFund: 84_729_310,
founderPool: 42_364_655,
},
freeReadsToday: 14,
freeReadsLimit: 20,
leaderboard: [
{ rank: 1, name: 'node-7f3a...c2e1', ruvEarned: 12847.5, tasks: 28341, tier: 'Guardian' },
{ rank: 2, name: 'node-b2d1...9f4a', ruvEarned: 9432.1, tasks: 21203, tier: 'Architect' },
{ rank: 3, name: 'node-e5c8...1b7d', ruvEarned: 7891.3, tasks: 17654, tier: 'Architect' },
{ rank: 4, name: 'node-a1f9...d3e5', ruvEarned: 5234.8, tasks: 12089, tier: 'Builder' },
{ rank: 5, name: 'node-c4b6...8a2f', ruvEarned: 3912.4, tasks: 8921, tier: 'Builder' },
],
velocity: 0.42,
utilization: 0.68,
stability: 0.91,
}));
useEffect(() => {
const interval = setInterval(() => {
setEconomics((prev) => {
const newMultiplier = Math.min(
3.0,
1.0 + Math.log1p(credits.earned * 0.02) * 1.2
);
return {
...prev,
contributionMultiplier: Math.round(newMultiplier * 100) / 100,
supply: {
...prev.supply,
totalMinted: prev.supply.totalMinted + Math.floor(Math.random() * 100),
},
freeReadsToday: Math.min(prev.freeReadsLimit, prev.freeReadsToday + (Math.random() > 0.95 ? 1 : 0)),
velocity: 0.42 + Math.sin(Date.now() / 10000) * 0.05,
utilization: Math.min(1.0, 0.68 + stats.tasksCompleted * 0.0001),
};
});
}, 3000);
return () => clearInterval(interval);
}, [credits.earned, stats.tasksCompleted]);
return economics;
}
const SUPPLY_COLORS = ['#38bdf8', '#a78bfa', '#06b6d4', '#fbbf24'];
const tierColorMap: Record<string, string> = {
Observer: 'text-zinc-400',
Contributor: 'text-sky-400',
Builder: 'text-violet-400',
Architect: 'text-amber-400',
Guardian: 'text-emerald-400',
};
export function EconomicsOverview() {
const eco = useEconomicsState();
const supplyPieData = useMemo(
() => [
{ name: 'Contributors (70%)', value: eco.supply.contributorPool },
{ name: 'Treasury (15%)', value: eco.supply.treasury },
{ name: 'Protocol (10%)', value: eco.supply.protocolFund },
{ name: 'Founders (5%)', value: eco.supply.founderPool },
],
[eco.supply]
);
const tierBarData = useMemo(
() =>
eco.reputationTiers.map((t) => ({
name: t.name,
count: t.count,
fill: t.color,
})),
[eco.reputationTiers]
);
const mintPercentage =
(eco.supply.totalMinted / eco.supply.maxSupply) * 100;
return (
<div className="space-y-6">
{/* Top Stats Row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="bg-gradient-to-br from-sky-500/20 to-sky-600/10 border border-sky-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<TrendingUp className="text-sky-400" size={22} />
<span className="text-xs text-sky-400/70">Your Rate</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{eco.contributionMultiplier.toFixed(2)}x
</p>
<p className="text-sm text-sky-400 mt-1">
Contribution Multiplier
</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 border border-emerald-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Gauge className="text-emerald-400" size={22} />
<span className="text-xs text-emerald-400/70">Health</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{(eco.stability * 100).toFixed(0)}%
</p>
<p className="text-sm text-emerald-400 mt-1">
Economic Stability
</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-gradient-to-br from-violet-500/20 to-violet-600/10 border border-violet-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<CircleDollarSign className="text-violet-400" size={22} />
<span className="text-xs text-violet-400/70">Velocity</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{(eco.velocity * 100).toFixed(0)}%
</p>
<p className="text-sm text-violet-400 mt-1">rUv Circulation</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="bg-gradient-to-br from-amber-500/20 to-amber-600/10 border border-amber-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Gift className="text-amber-400" size={22} />
<span className="text-xs text-amber-400/70">Free Tier</span>
</div>
<p className="text-3xl font-bold text-white stat-value">
{eco.freeReadsToday}/{eco.freeReadsLimit}
</p>
<p className="text-sm text-amber-400 mt-1">Free Reads Today</p>
</CardBody>
</Card>
</motion.div>
</div>
{/* Middle Row: Supply + Multiplier Curve */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* rUv Supply Distribution */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Coins className="text-sky-400" size={18} />
rUv Supply Distribution
</h3>
<div className="flex items-center gap-6">
<div className="w-40 h-40 flex-shrink-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={supplyPieData}
innerRadius={45}
outerRadius={70}
paddingAngle={3}
dataKey="value"
stroke="none"
>
{supplyPieData.map((_, idx) => (
<Cell key={idx} fill={SUPPLY_COLORS[idx]} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex-1 space-y-2">
{supplyPieData.map((entry, idx) => (
<div
key={entry.name}
className="flex items-center justify-between text-sm"
>
<div className="flex items-center gap-2">
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: SUPPLY_COLORS[idx] }}
/>
<span className="text-zinc-300">{entry.name}</span>
</div>
<span className="text-zinc-400 font-mono text-xs">
{(entry.value / 1_000_000).toFixed(1)}M
</span>
</div>
))}
<div className="pt-2 border-t border-white/10">
<div className="flex justify-between text-sm">
<span className="text-zinc-400">Minted / Max</span>
<span className="text-white font-medium">
{mintPercentage.toFixed(2)}%
</span>
</div>
<Progress
value={mintPercentage}
maxValue={100}
size="sm"
classNames={{
indicator:
'bg-gradient-to-r from-sky-500 via-violet-500 to-cyan-500',
track: 'bg-zinc-800',
}}
className="mt-2"
/>
</div>
</div>
</div>
</motion.div>
{/* Contribution Multiplier Curve */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="text-violet-400" size={18} />
Contribution Multiplier Curve
</h3>
<p className="text-xs text-zinc-500 mb-3">
Earn more rUv as your contribution score grows. Logarithmic curve
rewards early participants.
</p>
<div className="h-44">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={eco.multiplierCurve}>
<defs>
<linearGradient
id="multiplierGrad"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor="#a78bfa" stopOpacity={0.3} />
<stop offset="95%" stopColor="#a78bfa" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="x"
tick={{ fill: '#71717a', fontSize: 10 }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
label={{
value: 'Contribution Score',
position: 'insideBottom',
offset: -2,
fill: '#71717a',
fontSize: 10,
}}
/>
<YAxis
tick={{ fill: '#71717a', fontSize: 10 }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
domain={[0.5, 3.5]}
label={{
value: 'Multiplier',
angle: -90,
position: 'insideLeft',
offset: 10,
fill: '#71717a',
fontSize: 10,
}}
/>
<Tooltip
contentStyle={{
backgroundColor: '#18181b',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8,
fontSize: 12,
}}
formatter={(value: number) => [
`${value.toFixed(2)}x`,
'Multiplier',
]}
/>
<Area
type="monotone"
dataKey="y"
stroke="#a78bfa"
strokeWidth={2}
fill="url(#multiplierGrad)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-3 flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-violet-400" />
<span className="text-xs text-zinc-400">
Your current multiplier:{' '}
<span className="text-violet-400 font-semibold">
{eco.contributionMultiplier.toFixed(2)}x
</span>
</span>
</div>
</motion.div>
</div>
{/* Bottom Row: Reputation Tiers + Leaderboard */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Reputation Tier Distribution */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Shield className="text-amber-400" size={18} />
Reputation Tier Distribution
</h3>
<div className="h-44 mb-4">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={tierBarData} barSize={32}>
<XAxis
dataKey="name"
tick={{ fill: '#71717a', fontSize: 10 }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
/>
<YAxis
tick={{ fill: '#71717a', fontSize: 10 }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: '#18181b',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8,
fontSize: 12,
}}
formatter={(value: number) => [
value.toLocaleString(),
'Nodes',
]}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
{tierBarData.map((entry, idx) => (
<Cell key={idx} fill={entry.fill} fillOpacity={0.7} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="space-y-2">
{eco.reputationTiers.map((tier) => (
<div
key={tier.name}
className="flex items-center justify-between text-sm"
>
<div className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tier.color }}
/>
<span className="text-zinc-300">{tier.name}</span>
<span className="text-xs text-zinc-600">
({'>'}
{(tier.minScore * 100).toFixed(0)}%)
</span>
</div>
<span className="text-xs text-zinc-500">{tier.benefits}</span>
</div>
))}
</div>
</motion.div>
{/* Top Earners Leaderboard */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Award className="text-emerald-400" size={18} />
Top Earners
</h3>
<div className="space-y-3">
{eco.leaderboard.map((entry) => (
<div
key={entry.rank}
className="flex items-center gap-3 p-3 rounded-lg bg-zinc-800/50"
>
<div
className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold ${
entry.rank === 1
? 'bg-amber-500/20 text-amber-400'
: entry.rank === 2
? 'bg-zinc-400/20 text-zinc-300'
: entry.rank === 3
? 'bg-orange-500/20 text-orange-400'
: 'bg-zinc-700/50 text-zinc-500'
}`}
>
{entry.rank}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-mono text-zinc-300 truncate">
{entry.name}
</p>
<p className="text-xs text-zinc-500">
{entry.tasks.toLocaleString()} tasks
</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-emerald-400">
{entry.ruvEarned.toLocaleString()} rUv
</p>
<p
className={`text-xs ${tierColorMap[entry.tier] || 'text-zinc-400'}`}
>
{entry.tier}
</p>
</div>
</div>
))}
</div>
<div className="mt-4 p-3 rounded-lg bg-sky-500/10 border border-sky-500/20">
<div className="flex items-center gap-2">
<Users className="text-sky-400" size={14} />
<span className="text-xs text-sky-400">
{eco.reputationTiers
.reduce((sum, t) => sum + t.count, 0)
.toLocaleString()}{' '}
total network participants
</span>
</div>
</div>
</motion.div>
</div>
</div>
);
}

View file

@ -0,0 +1,452 @@
import { Card, CardBody } from '@heroui/react';
import { motion } from 'framer-motion';
import {
Gift,
ArrowRight,
BookOpen,
PenTool,
Upload,
Shield,
Star,
CheckCircle2,
Zap,
TrendingUp,
Users,
Cpu,
} from 'lucide-react';
// Step-by-step how to earn
interface EarnStep {
step: number;
title: string;
description: string;
icon: React.ReactNode;
color: string;
}
const earnSteps: EarnStep[] = [
{
step: 1,
title: 'Enable Contribution',
description:
'Toggle on compute sharing from the consent widget. Your browser contributes idle CPU cycles to the network.',
icon: <Cpu size={20} />,
color: 'sky',
},
{
step: 2,
title: 'Complete Tasks',
description:
'Your node automatically picks up and processes tasks from the network. Each completed task earns rUv.',
icon: <CheckCircle2 size={20} />,
color: 'emerald',
},
{
step: 3,
title: 'Build Reputation',
description:
'Consistent, accurate contributions increase your reputation score, unlocking higher tiers and multipliers.',
icon: <TrendingUp size={20} />,
color: 'violet',
},
{
step: 4,
title: 'Contribute Knowledge',
description:
'Share knowledge patterns to the Brain. Quality contributions earn bonus rUv through the earn-to-write model.',
icon: <Upload size={20} />,
color: 'amber',
},
];
// Reward table data
interface RewardAction {
action: string;
reward: string;
cost: string;
frequency: string;
icon: React.ReactNode;
}
const rewardTable: RewardAction[] = [
{
action: 'Read from Brain',
reward: 'Free',
cost: '0 rUv',
frequency: '20/day free, then 0.001 rUv',
icon: <BookOpen size={16} className="text-sky-400" />,
},
{
action: 'Write to Brain',
reward: '0.01-0.10 rUv',
cost: '0 rUv (earn-to-write)',
frequency: 'Per accepted entry',
icon: <PenTool size={16} className="text-emerald-400" />,
},
{
action: 'Compute Task',
reward: '0.001-0.05 rUv',
cost: 'CPU/GPU time',
frequency: 'Per completed task',
icon: <Cpu size={16} className="text-violet-400" />,
},
{
action: 'Uptime Bonus',
reward: '0.005 rUv/hr',
cost: 'Stay online',
frequency: 'Continuous',
icon: <Zap size={16} className="text-amber-400" />,
},
{
action: 'Quality Bonus',
reward: 'Up to 3x multiplier',
cost: 'High accuracy',
frequency: 'Applied to all earnings',
icon: <Star size={16} className="text-pink-400" />,
},
{
action: 'Network Referral',
reward: '5% of referee earnings',
cost: 'Share your node link',
frequency: 'Ongoing for 90 days',
icon: <Users size={16} className="text-cyan-400" />,
},
];
// Tier benefits comparison
interface TierBenefit {
tier: string;
color: string;
bgClass: string;
multiplier: string;
freeReads: string;
writePriority: string;
governance: boolean;
specialAccess: string;
}
const tierBenefits: TierBenefit[] = [
{
tier: 'Observer',
color: 'text-zinc-400',
bgClass: 'bg-zinc-500/10 border-zinc-500/30',
multiplier: '1.0x',
freeReads: '20/day',
writePriority: 'Standard',
governance: false,
specialAccess: 'None',
},
{
tier: 'Contributor',
color: 'text-sky-400',
bgClass: 'bg-sky-500/10 border-sky-500/30',
multiplier: '1.2x',
freeReads: '50/day',
writePriority: 'Standard',
governance: false,
specialAccess: 'Metrics API',
},
{
tier: 'Builder',
color: 'text-violet-400',
bgClass: 'bg-violet-500/10 border-violet-500/30',
multiplier: '1.8x',
freeReads: '100/day',
writePriority: 'Priority',
governance: false,
specialAccess: 'Advanced search',
},
{
tier: 'Architect',
color: 'text-amber-400',
bgClass: 'bg-amber-500/10 border-amber-500/30',
multiplier: '2.5x',
freeReads: 'Unlimited',
writePriority: 'Priority+',
governance: true,
specialAccess: 'Custom models',
},
{
tier: 'Guardian',
color: 'text-emerald-400',
bgClass: 'bg-emerald-500/10 border-emerald-500/30',
multiplier: '3.0x',
freeReads: 'Unlimited',
writePriority: 'Instant',
governance: true,
specialAccess: 'Full API + governance',
},
];
export function RewardsGuide() {
return (
<div className="space-y-6">
{/* How to Earn Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="crystal-card p-6">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-emerald-500/20">
<Gift className="text-emerald-400" size={24} />
</div>
<div>
<h2 className="text-xl font-bold text-white">How to Earn rUv</h2>
<p className="text-sm text-zinc-400">
rUv is the native token of the Edge-Net economy. Everyone starts
earning for free.
</p>
</div>
</div>
</div>
</motion.div>
{/* Step-by-Step Guide */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{earnSteps.map((step, idx) => {
const colorMap: Record<string, string> = {
sky: 'from-sky-500/20 to-sky-600/10 border-sky-500/30',
emerald:
'from-emerald-500/20 to-emerald-600/10 border-emerald-500/30',
violet:
'from-violet-500/20 to-violet-600/10 border-violet-500/30',
amber: 'from-amber-500/20 to-amber-600/10 border-amber-500/30',
};
const iconColorMap: Record<string, string> = {
sky: 'text-sky-400 bg-sky-500/20',
emerald: 'text-emerald-400 bg-emerald-500/20',
violet: 'text-violet-400 bg-violet-500/20',
amber: 'text-amber-400 bg-amber-500/20',
};
return (
<motion.div
key={step.step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }}
>
<Card
className={`bg-gradient-to-br ${colorMap[step.color]} border h-full`}
>
<CardBody className="p-5">
<div className="flex items-center gap-2 mb-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${iconColorMap[step.color]}`}
>
{step.step}
</div>
{idx < earnSteps.length - 1 && (
<ArrowRight
size={12}
className="text-zinc-600 hidden sm:block"
/>
)}
</div>
<div className={`mb-3 ${iconColorMap[step.color]} w-fit p-2 rounded-lg`}>
{step.icon}
</div>
<h3 className="font-semibold text-white mb-1">
{step.title}
</h3>
<p className="text-xs text-zinc-400 leading-relaxed">
{step.description}
</p>
</CardBody>
</Card>
</motion.div>
);
})}
</div>
{/* Reward Actions Table */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Zap className="text-amber-400" size={18} />
Reward Table
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-2 px-3 text-zinc-500 font-medium">
Action
</th>
<th className="text-left py-2 px-3 text-zinc-500 font-medium">
Reward
</th>
<th className="text-left py-2 px-3 text-zinc-500 font-medium">
Cost
</th>
<th className="text-left py-2 px-3 text-zinc-500 font-medium">
Details
</th>
</tr>
</thead>
<tbody>
{rewardTable.map((row) => (
<tr
key={row.action}
className="border-b border-white/5 hover:bg-white/5 transition-colors"
>
<td className="py-3 px-3">
<div className="flex items-center gap-2">
{row.icon}
<span className="text-zinc-300">{row.action}</span>
</div>
</td>
<td className="py-3 px-3">
<span className="text-emerald-400 font-medium">
{row.reward}
</span>
</td>
<td className="py-3 px-3">
<span className="text-zinc-400">{row.cost}</span>
</td>
<td className="py-3 px-3">
<span className="text-zinc-500 text-xs">
{row.frequency}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</motion.div>
{/* Tier Benefits Comparison */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Shield className="text-violet-400" size={18} />
Tier Benefits Comparison
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-2 px-3 text-zinc-500 font-medium">
Tier
</th>
<th className="text-center py-2 px-3 text-zinc-500 font-medium">
Multiplier
</th>
<th className="text-center py-2 px-3 text-zinc-500 font-medium">
Free Reads
</th>
<th className="text-center py-2 px-3 text-zinc-500 font-medium">
Write Priority
</th>
<th className="text-center py-2 px-3 text-zinc-500 font-medium">
Governance
</th>
<th className="text-center py-2 px-3 text-zinc-500 font-medium">
Special Access
</th>
</tr>
</thead>
<tbody>
{tierBenefits.map((tier) => (
<tr
key={tier.tier}
className="border-b border-white/5 hover:bg-white/5 transition-colors"
>
<td className="py-3 px-3">
<span className={`font-semibold ${tier.color}`}>
{tier.tier}
</span>
</td>
<td className="py-3 px-3 text-center">
<span
className={`px-2 py-0.5 rounded-full text-xs border ${tier.bgClass} ${tier.color}`}
>
{tier.multiplier}
</span>
</td>
<td className="py-3 px-3 text-center text-zinc-300">
{tier.freeReads}
</td>
<td className="py-3 px-3 text-center text-zinc-300">
{tier.writePriority}
</td>
<td className="py-3 px-3 text-center">
{tier.governance ? (
<CheckCircle2 size={16} className="text-emerald-400 mx-auto" />
) : (
<span className="text-zinc-600">-</span>
)}
</td>
<td className="py-3 px-3 text-center text-zinc-400 text-xs">
{tier.specialAccess}
</td>
</tr>
))}
</tbody>
</table>
</div>
</motion.div>
{/* Key Principles */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Star className="text-pink-400" size={18} />
Key Economic Principles
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-sky-500/10 border border-sky-500/20">
<h4 className="font-medium text-sky-400 mb-1">
Always Free to Start
</h4>
<p className="text-xs text-zinc-400">
No upfront cost. Free reads every day. Start earning immediately by
contributing compute or knowledge.
</p>
</div>
<div className="p-4 rounded-lg bg-violet-500/10 border border-violet-500/20">
<h4 className="font-medium text-violet-400 mb-1">
Earn-to-Write Model
</h4>
<p className="text-xs text-zinc-400">
Writing to the Brain is not a cost -- it is a reward. Quality
knowledge contributions earn rUv.
</p>
</div>
<div className="p-4 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<h4 className="font-medium text-emerald-400 mb-1">
Halving Epochs
</h4>
<p className="text-xs text-zinc-400">
Like Bitcoin, mining rewards halve periodically. Early contributors
earn more rUv per unit of work.
</p>
</div>
<div className="p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<h4 className="font-medium text-amber-400 mb-1">
Reputation Matters
</h4>
<p className="text-xs text-zinc-400">
Reputation decays over time to prevent gaming. Consistent quality
contributions keep your multiplier high.
</p>
</div>
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,489 @@
/**
* Edge-Net Relay Brain API Bridge — Tests
*
* Tests the relay server's WebSocket handling, identity derivation,
* rate limiting, rUv accounting, and brain API proxy routing.
*
* Uses Node.js built-in test runner (node:test).
*/
import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import http from 'node:http';
import { WebSocket } from 'ws';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Generate a valid 32-byte Ed25519 public key (hex) for testing. */
function makePublicKey(seed = 0) {
const buf = Buffer.alloc(32);
buf[0] = seed & 0xff;
buf[1] = (seed >> 8) & 0xff;
buf.fill(0xab, 2);
return buf.toString('hex');
}
/** Derive expected SHAKE-256 pseudonym for a key (mirrors relay logic). */
function expectedPseudonym(publicKeyHex) {
const h = createHash('shake256', { outputLength: 16 });
h.update(Buffer.from(publicKeyHex, 'hex'));
return h.digest('hex');
}
/**
* Connect a WebSocket to the relay and optionally authenticate.
* @param {number} port
* @param {string|null} publicKey - If provided, sends auth message and waits for auth_result.
* @returns {Promise<{ws: WebSocket, messages: Object[], pseudonym: string|null}>}
*/
function connectClient(port, publicKey = null) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
const messages = [];
let pseudonym = null;
ws.on('error', reject);
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
messages.push(msg);
// After receiving welcome, optionally authenticate.
if (msg.type === 'welcome' && publicKey) {
ws.send(JSON.stringify({ id: 'auth-1', type: 'auth', payload: { public_key: publicKey } }));
}
if (msg.type === 'auth_result' && msg.ok) {
pseudonym = msg.data.pseudonym;
resolve({ ws, messages, pseudonym });
}
if (msg.type === 'auth_result' && !msg.ok) {
resolve({ ws, messages, pseudonym: null });
}
});
// If no auth needed, resolve after welcome.
if (!publicKey) {
ws.on('open', () => {
// Wait for welcome message.
const check = setInterval(() => {
if (messages.length > 0) {
clearInterval(check);
resolve({ ws, messages, pseudonym: null });
}
}, 10);
});
}
});
}
/**
* Send a message and wait for a response of the expected type.
* @param {WebSocket} ws
* @param {Object} msg
* @param {string} expectedType
* @param {number} timeout
* @returns {Promise<Object>}
*/
function sendAndWait(ws, msg, expectedType, timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${expectedType}`)), timeout);
const handler = (data) => {
const parsed = JSON.parse(data.toString());
if (parsed.type === expectedType) {
ws.off('message', handler);
clearTimeout(timer);
resolve(parsed);
}
};
ws.on('message', handler);
ws.send(JSON.stringify(msg));
});
}
/** Fetch JSON from the relay's HTTP endpoint. */
async function httpGet(port, path) {
return new Promise((resolve, reject) => {
http.get(`http://127.0.0.1:${port}${path}`, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(body) });
} catch {
resolve({ status: res.statusCode, data: body });
}
});
}).on('error', reject);
});
}
// ---------------------------------------------------------------------------
// Test Suite
// ---------------------------------------------------------------------------
describe('Edge-Net Relay Brain API Bridge', () => {
/** @type {import('child_process').ChildProcess} */
let relayProcess;
let relayPort;
before(async () => {
// Start the relay on a random port for test isolation.
relayPort = 10000 + Math.floor(Math.random() * 50000);
const { spawn } = await import('node:child_process');
relayProcess = spawn('node', ['index.js'], {
cwd: '/workspaces/ruvector/examples/edge-net/relay',
env: { ...process.env, PORT: String(relayPort), BRAIN_API_BASE: 'http://127.0.0.1:19999' },
stdio: ['pipe', 'pipe', 'pipe'],
});
// Wait for the relay to start listening.
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Relay failed to start within 5s')), 5000);
relayProcess.stdout.on('data', (data) => {
if (data.toString().includes('listening on port')) {
clearTimeout(timer);
resolve();
}
});
relayProcess.stderr.on('data', (data) => {
// Log stderr but don't fail — Node warnings are fine.
});
relayProcess.on('exit', (code) => {
if (code !== null) {
clearTimeout(timer);
reject(new Error(`Relay exited with code ${code}`));
}
});
});
});
after(() => {
if (relayProcess) {
relayProcess.kill('SIGTERM');
}
});
// ---- HTTP Health Endpoint ----
describe('HTTP health endpoint', () => {
it('returns health status at /', async () => {
const { status, data } = await httpGet(relayPort, '/');
assert.equal(status, 200);
assert.equal(data.status, 'ok');
assert.equal(data.service, 'edge-net-relay');
assert.equal(data.version, '0.2.0');
assert.equal(typeof data.connected_nodes, 'number');
assert.equal(typeof data.uptime_seconds, 'number');
});
it('returns health status at /health', async () => {
const { status, data } = await httpGet(relayPort, '/health');
assert.equal(status, 200);
assert.equal(data.status, 'ok');
});
it('returns stats at /stats', async () => {
const { status, data } = await httpGet(relayPort, '/stats');
assert.equal(status, 200);
assert.equal(typeof data.nodes, 'number');
assert.ok(data.rate_limits);
assert.equal(data.rate_limits.reads_per_hour, 1000);
assert.equal(data.rate_limits.writes_per_hour, 100);
});
it('returns 404 for unknown paths', async () => {
const { status, data } = await httpGet(relayPort, '/nonexistent');
assert.equal(status, 404);
assert.equal(data.error, 'Not found');
});
});
// ---- WebSocket Connection & Auth ----
describe('WebSocket auth handshake', () => {
it('sends welcome message on connect', async () => {
const { ws, messages } = await connectClient(relayPort);
assert.equal(messages[0].type, 'welcome');
assert.ok(messages[0].data.supported_types);
assert.equal(messages[0].data.auth_required, true);
ws.close();
});
it('authenticates with valid Pi-Key public key', async () => {
const pubKey = makePublicKey(1);
const { ws, pseudonym } = await connectClient(relayPort, pubKey);
assert.ok(pseudonym);
assert.equal(pseudonym, expectedPseudonym(pubKey));
assert.equal(pseudonym.length, 32); // 16 bytes = 32 hex chars
ws.close();
});
it('rejects invalid public key (wrong length)', async () => {
const { ws, messages } = await connectClient(relayPort);
const resp = await sendAndWait(
ws,
{ id: 'bad-auth', type: 'auth', payload: { public_key: 'deadbeef' } },
'auth_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error.includes('Invalid public key'));
ws.close();
});
it('rejects non-hex public key', async () => {
const { ws, messages } = await connectClient(relayPort);
const resp = await sendAndWait(
ws,
{ id: 'bad-auth2', type: 'auth', payload: { public_key: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' } },
'auth_result',
);
assert.equal(resp.ok, false);
ws.close();
});
it('rejects operations before auth', async () => {
const { ws } = await connectClient(relayPort);
const resp = await sendAndWait(
ws,
{ id: 'pre-auth', type: 'brain_status', payload: {} },
'brain_status_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error.includes('Not authenticated'));
ws.close();
});
});
// ---- Identity Derivation ----
describe('SHAKE-256 pseudonym derivation', () => {
it('produces deterministic pseudonyms', async () => {
const pubKey = makePublicKey(42);
const { ws: ws1, pseudonym: p1 } = await connectClient(relayPort, pubKey);
ws1.close();
// Allow cleanup
await new Promise((r) => setTimeout(r, 100));
const { ws: ws2, pseudonym: p2 } = await connectClient(relayPort, pubKey);
assert.equal(p1, p2);
ws2.close();
});
it('produces different pseudonyms for different keys', async () => {
const { ws: ws1, pseudonym: p1 } = await connectClient(relayPort, makePublicKey(1));
const { ws: ws2, pseudonym: p2 } = await connectClient(relayPort, makePublicKey(2));
assert.notEqual(p1, p2);
ws1.close();
ws2.close();
});
});
// ---- rUv Accounting (local operations) ----
describe('rUv accounting', () => {
it('returns zero balance for new nodes', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(100));
const resp = await sendAndWait(ws, { id: 'bal-1', type: 'ruv_balance', payload: {} }, 'ruv_balance_result');
assert.equal(resp.ok, true);
assert.equal(resp.data.balance, 0);
assert.equal(resp.data.operations, 0);
ws.close();
});
it('credits rUv via ruv_earn', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(101));
const earnResp = await sendAndWait(
ws,
{ id: 'earn-1', type: 'ruv_earn', payload: { amount: 3.5, reason: 'embedding_gen' } },
'ruv_earn_result',
);
assert.equal(earnResp.ok, true);
assert.equal(earnResp.data.credited, 3.5);
assert.equal(earnResp.data.balance, 3.5);
const balResp = await sendAndWait(ws, { id: 'bal-2', type: 'ruv_balance', payload: {} }, 'ruv_balance_result');
assert.equal(balResp.data.balance, 3.5);
assert.equal(balResp.data.operations, 1);
ws.close();
});
it('rejects ruv_earn with non-positive amount', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(102));
const resp = await sendAndWait(
ws,
{ id: 'earn-bad', type: 'ruv_earn', payload: { amount: -1, reason: 'hack' } },
'ruv_earn_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error.includes('positive'));
ws.close();
});
it('accumulates multiple earn operations', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(103));
await sendAndWait(ws, { id: 'e1', type: 'ruv_earn', payload: { amount: 1.0, reason: 'a' } }, 'ruv_earn_result');
await sendAndWait(ws, { id: 'e2', type: 'ruv_earn', payload: { amount: 2.0, reason: 'b' } }, 'ruv_earn_result');
await sendAndWait(ws, { id: 'e3', type: 'ruv_earn', payload: { amount: 0.5, reason: 'c' } }, 'ruv_earn_result');
const balResp = await sendAndWait(ws, { id: 'bal-3', type: 'ruv_balance', payload: {} }, 'ruv_balance_result');
assert.equal(balResp.data.balance, 3.5);
assert.equal(balResp.data.operations, 3);
ws.close();
});
});
// ---- Brain API Proxy (with mock brain unavailable) ----
// Since the mock brain API at 127.0.0.1:19999 is not running, these
// should return errors from the fetch failure, testing error handling.
describe('brain API proxy (brain offline)', () => {
it('handles brain_status when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(200));
const resp = await sendAndWait(
ws,
{ id: 'status-1', type: 'brain_status', payload: {} },
'brain_status_result',
);
// Brain is offline, so we expect an error response (not a crash).
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('handles brain_search when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(201));
const resp = await sendAndWait(
ws,
{ id: 'search-1', type: 'brain_search', payload: { query: 'test query', limit: 5 } },
'brain_search_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('handles brain_share when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(202));
const resp = await sendAndWait(
ws,
{ id: 'share-1', type: 'brain_share', payload: { title: 'Test', content: 'Hello', category: 'debug' } },
'brain_share_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('handles brain_vote when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(203));
const resp = await sendAndWait(
ws,
{ id: 'vote-1', type: 'brain_vote', payload: { id: 'mem-123', direction: 'up' } },
'brain_vote_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('handles brain_list when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(204));
const resp = await sendAndWait(
ws,
{ id: 'list-1', type: 'brain_list', payload: { limit: 10 } },
'brain_list_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('handles brain_lora_latest when brain is unreachable', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(205));
const resp = await sendAndWait(
ws,
{ id: 'lora-1', type: 'brain_lora_latest', payload: {} },
'brain_lora_latest_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error);
ws.close();
});
it('rejects unknown message types', async () => {
const { ws } = await connectClient(relayPort, makePublicKey(206));
const resp = await sendAndWait(
ws,
{ id: 'bad-1', type: 'nonexistent_op', payload: {} },
'nonexistent_op_result',
);
assert.equal(resp.ok, false);
assert.ok(resp.error.includes('Unknown'));
ws.close();
});
});
// ---- Invalid JSON handling ----
describe('error handling', () => {
it('handles invalid JSON gracefully', async () => {
const { ws, messages } = await connectClient(relayPort);
// Wait for welcome
await new Promise((r) => setTimeout(r, 50));
return new Promise((resolve) => {
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'error') {
assert.equal(msg.ok, false);
assert.ok(msg.error.includes('Invalid JSON'));
ws.close();
resolve();
}
});
ws.send('not valid json {{{');
});
});
});
// ---- Concurrent connections ----
describe('concurrent connections', () => {
it('handles multiple simultaneous clients', async () => {
const clients = await Promise.all([
connectClient(relayPort, makePublicKey(300)),
connectClient(relayPort, makePublicKey(301)),
connectClient(relayPort, makePublicKey(302)),
]);
// Each client should have a unique pseudonym.
const pseudonyms = new Set(clients.map((c) => c.pseudonym));
assert.equal(pseudonyms.size, 3);
// Each client can query rUv balance independently.
const results = await Promise.all(
clients.map((c) =>
sendAndWait(c.ws, { id: 'bal', type: 'ruv_balance', payload: {} }, 'ruv_balance_result'),
),
);
for (const r of results) {
assert.equal(r.ok, true);
assert.equal(r.data.balance, 0);
}
for (const c of clients) c.ws.close();
});
});
});

View file

@ -0,0 +1,558 @@
//! Brain Integration Module
//!
//! Bridges edge-net distributed compute with pi brain shared intelligence.
//! Edge nodes interact with the brain through the relay WebSocket connection.
//!
//! Since this runs in WASM, actual WebSocket calls go through JavaScript interop.
//! The BrainBridge prepares request JSON that the JavaScript host sends through
//! the relay WebSocket. Each method returns a JSON string representing the
//! request to be sent.
//!
//! ## rUv Rewards
//!
//! Brain operations earn rUv (Resource Utility Vouchers):
//! - `search`: 0.5 rUv per query
//! - `share`: 5.0 rUv per knowledge contribution
//! - `vote`: 0.2 rUv per quality vote
//! - `lora_pull`: 0.1 rUv per LoRA weight pull
//!
//! ## Usage
//!
//! ```rust,ignore
//! use ruvector_edge_net::brain::BrainBridge;
//!
//! let bridge = BrainBridge::new("ws://localhost:8080/relay", "node-abc123");
//!
//! // Get a search request to send via JS WebSocket
//! let request_json = bridge.brain_search("authentication patterns", 10);
//! // JS host sends request_json through relay WebSocket
//! ```
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use serde_json::json;
/// Get current timestamp in milliseconds (portable across wasm/native)
fn now_ms() -> f64 {
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now()
}
#[cfg(not(target_arch = "wasm32"))]
{
0.0
}
}
/// rUv reward amounts for brain operations
mod rewards {
pub const SEARCH: f64 = 0.5;
pub const SHARE: f64 = 5.0;
pub const VOTE: f64 = 0.2;
pub const LORA_PULL: f64 = 0.1;
pub const STATUS: f64 = 0.0;
pub const LIST: f64 = 0.1;
}
/// Brain operation types that edge nodes can perform
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum BrainOperation {
/// Search shared brain knowledge
Search { query: String, limit: usize },
/// Share knowledge with the brain
Share {
title: String,
content: String,
category: String,
tags: Vec<String>,
},
/// Vote on brain knowledge quality
Vote {
memory_id: String,
/// "up" or "down"
direction: String,
},
/// Get brain system status
Status,
/// List brain memories by category
List {
category: Option<String>,
limit: usize,
},
/// Get latest LoRA consensus weights
LoraLatest,
}
impl BrainOperation {
/// Get the rUv reward for this operation type
pub fn ruv_reward(&self) -> f64 {
match self {
BrainOperation::Search { .. } => rewards::SEARCH,
BrainOperation::Share { .. } => rewards::SHARE,
BrainOperation::Vote { .. } => rewards::VOTE,
BrainOperation::Status => rewards::STATUS,
BrainOperation::List { .. } => rewards::LIST,
BrainOperation::LoraLatest => rewards::LORA_PULL,
}
}
/// Get the operation name as a string
pub fn name(&self) -> &'static str {
match self {
BrainOperation::Search { .. } => "search",
BrainOperation::Share { .. } => "share",
BrainOperation::Vote { .. } => "vote",
BrainOperation::Status => "status",
BrainOperation::List { .. } => "list",
BrainOperation::LoraLatest => "lora_latest",
}
}
}
/// Result from a brain operation
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BrainResult {
/// Whether the operation succeeded
pub success: bool,
/// Result data (operation-specific)
pub data: serde_json::Value,
/// rUv earned from this operation
pub ruv_earned: f64,
/// Operation type name
pub operation: String,
}
/// Record of a completed brain operation for tracking
#[derive(Clone, Debug, Serialize, Deserialize)]
struct BrainOpRecord {
/// Operation type
operation: String,
/// rUv earned
ruv_earned: f64,
/// Timestamp (ms since epoch)
timestamp: f64,
/// Whether the operation succeeded
success: bool,
}
/// Brain integration client for edge-net nodes
///
/// Prepares brain operation requests as JSON for the JavaScript host
/// to send through the relay WebSocket. Tracks rUv earned and
/// operation history locally.
#[wasm_bindgen]
pub struct BrainBridge {
/// Relay WebSocket URL
relay_url: String,
/// Pi-Key identity for attribution
node_identity: String,
/// Accumulated rUv from brain operations
brain_ruv_earned: f64,
/// Total operations performed
operations_count: u64,
/// Quality score from brain interactions (0.0 - 1.0)
brain_reputation: f32,
/// Operation history for audit
history: Vec<BrainOpRecord>,
}
#[wasm_bindgen]
impl BrainBridge {
/// Create a new BrainBridge for connecting to the shared brain
///
/// # Arguments
/// * `relay_url` - WebSocket URL of the relay server
/// * `node_identity` - Pi-Key identity hex string for attribution
#[wasm_bindgen(constructor)]
pub fn new(relay_url: &str, node_identity: &str) -> BrainBridge {
BrainBridge {
relay_url: relay_url.to_string(),
node_identity: node_identity.to_string(),
brain_ruv_earned: 0.0,
operations_count: 0,
brain_reputation: 0.5, // Start neutral
history: Vec::new(),
}
}
/// Search the shared brain knowledge
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// Earns 0.5 rUv per search.
#[wasm_bindgen(js_name = brainSearch)]
pub fn brain_search(&mut self, query: &str, limit: usize) -> String {
let op = BrainOperation::Search {
query: query.to_string(),
limit: limit.min(100), // Cap limit
};
self.prepare_request(op)
}
/// Share knowledge with the brain
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// Earns 5.0 rUv per share (highest reward for contributing knowledge).
///
/// # Arguments
/// * `title` - Title of the knowledge being shared
/// * `content` - The knowledge content
/// * `category` - Category (e.g., "pattern", "optimization", "security")
/// * `tags_json` - JSON array of tags (e.g., `["rust", "wasm"]`)
#[wasm_bindgen(js_name = brainShare)]
pub fn brain_share(
&mut self,
title: &str,
content: &str,
category: &str,
tags_json: &str,
) -> String {
let tags: Vec<String> = serde_json::from_str(tags_json).unwrap_or_default();
let op = BrainOperation::Share {
title: title.to_string(),
content: content.to_string(),
category: category.to_string(),
tags,
};
self.prepare_request(op)
}
/// Vote on brain knowledge quality
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// Earns 0.2 rUv per vote.
///
/// # Arguments
/// * `memory_id` - ID of the memory to vote on
/// * `direction` - "up" or "down"
#[wasm_bindgen(js_name = brainVote)]
pub fn brain_vote(&mut self, memory_id: &str, direction: &str) -> String {
// Validate direction
let direction = match direction {
"up" | "down" => direction.to_string(),
_ => "up".to_string(), // Default to upvote
};
let op = BrainOperation::Vote {
memory_id: memory_id.to_string(),
direction,
};
self.prepare_request(op)
}
/// Get brain system status
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// No rUv reward for status checks.
#[wasm_bindgen(js_name = brainStatus)]
pub fn brain_status(&mut self) -> String {
self.prepare_request(BrainOperation::Status)
}
/// List brain memories by category
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// Earns 0.1 rUv per list query.
///
/// # Arguments
/// * `category` - Category to filter by (empty string for all)
/// * `limit` - Maximum number of results
#[wasm_bindgen(js_name = brainList)]
pub fn brain_list(&mut self, category: &str, limit: usize) -> String {
let category = if category.is_empty() {
None
} else {
Some(category.to_string())
};
let op = BrainOperation::List {
category,
limit: limit.min(100),
};
self.prepare_request(op)
}
/// Get latest LoRA consensus weights
///
/// Returns a JSON request string for the JS host to send via WebSocket.
/// Earns 0.1 rUv per pull.
#[wasm_bindgen(js_name = brainLoraLatest)]
pub fn brain_lora_latest(&mut self) -> String {
self.prepare_request(BrainOperation::LoraLatest)
}
/// Get total rUv earned from brain operations
#[wasm_bindgen(js_name = getBrainRuv)]
pub fn get_brain_ruv(&self) -> f64 {
self.brain_ruv_earned
}
/// Get total brain operation count
#[wasm_bindgen(js_name = getBrainOpsCount)]
pub fn get_brain_ops_count(&self) -> u64 {
self.operations_count
}
/// Get brain reputation score (0.0 - 1.0)
#[wasm_bindgen(js_name = getBrainReputation)]
pub fn get_brain_reputation(&self) -> f32 {
self.brain_reputation
}
/// Get relay URL
#[wasm_bindgen(js_name = getRelayUrl)]
pub fn get_relay_url(&self) -> String {
self.relay_url.clone()
}
/// Get node identity
#[wasm_bindgen(js_name = getNodeIdentity)]
pub fn get_node_identity(&self) -> String {
self.node_identity.clone()
}
/// Record an operation result (called by JS after WebSocket response)
///
/// Updates rUv earned, operation count, and reputation based on
/// the result of a brain operation.
///
/// # Arguments
/// * `result_json` - JSON string with the BrainResult from the relay
#[wasm_bindgen(js_name = recordResult)]
pub fn record_result(&mut self, result_json: &str) -> bool {
let result: BrainResult = match serde_json::from_str(result_json) {
Ok(r) => r,
Err(_) => return false,
};
let timestamp = now_ms();
if result.success {
self.brain_ruv_earned += result.ruv_earned;
self.operations_count += 1;
// Improve reputation on success (weighted moving average)
self.brain_reputation = self.brain_reputation * 0.95 + 0.05;
} else {
// Slight reputation decrease on failure
self.brain_reputation = self.brain_reputation * 0.98;
}
// Clamp reputation
self.brain_reputation = self.brain_reputation.clamp(0.0, 1.0);
self.history.push(BrainOpRecord {
operation: result.operation,
ruv_earned: result.ruv_earned,
timestamp,
success: result.success,
});
// Keep history bounded (last 1000 operations)
if self.history.len() > 1000 {
self.history.drain(0..self.history.len() - 1000);
}
true
}
/// Get operation history as JSON
#[wasm_bindgen(js_name = getHistory)]
pub fn get_history(&self, limit: usize) -> String {
let limit = limit.min(self.history.len());
let recent: Vec<&BrainOpRecord> = self.history.iter().rev().take(limit).collect();
serde_json::to_string(&recent).unwrap_or_else(|_| "[]".to_string())
}
/// Get brain bridge statistics as JSON
#[wasm_bindgen(js_name = getStats)]
pub fn get_stats(&self) -> String {
let stats = json!({
"relay_url": self.relay_url,
"node_identity": self.node_identity,
"ruv_earned": self.brain_ruv_earned,
"operations_count": self.operations_count,
"brain_reputation": self.brain_reputation,
"history_size": self.history.len(),
});
stats.to_string()
}
}
impl BrainBridge {
/// Prepare a brain operation request as JSON for the relay WebSocket
///
/// The returned JSON has the format:
/// ```json
/// {
/// "type": "brain_request",
/// "relay_url": "ws://...",
/// "node_identity": "...",
/// "operation": { ... },
/// "ruv_reward": 0.5,
/// "request_id": "..."
/// }
/// ```
fn prepare_request(&mut self, op: BrainOperation) -> String {
let ruv_reward = op.ruv_reward();
let op_name = op.name().to_string();
let request_id = format!(
"brain-{}-{}",
self.operations_count,
now_ms() as u64
);
let request = json!({
"type": "brain_request",
"relay_url": self.relay_url,
"node_identity": self.node_identity,
"operation": op,
"operation_name": op_name,
"ruv_reward": ruv_reward,
"request_id": request_id,
});
request.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_brain_bridge_creation() {
let bridge = BrainBridge::new("ws://localhost:8080/relay", "node-abc123");
assert_eq!(bridge.get_relay_url(), "ws://localhost:8080/relay");
assert_eq!(bridge.get_node_identity(), "node-abc123");
assert_eq!(bridge.get_brain_ruv(), 0.0);
assert_eq!(bridge.get_brain_ops_count(), 0);
assert!((bridge.get_brain_reputation() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_brain_operation_rewards() {
assert!((BrainOperation::Search {
query: "test".into(),
limit: 10,
}.ruv_reward() - 0.5).abs() < f64::EPSILON);
assert!((BrainOperation::Share {
title: "t".into(),
content: "c".into(),
category: "cat".into(),
tags: vec![],
}.ruv_reward() - 5.0).abs() < f64::EPSILON);
assert!((BrainOperation::Vote {
memory_id: "m".into(),
direction: "up".into(),
}.ruv_reward() - 0.2).abs() < f64::EPSILON);
assert!((BrainOperation::LoraLatest.ruv_reward() - 0.1).abs() < f64::EPSILON);
assert!((BrainOperation::Status.ruv_reward()).abs() < f64::EPSILON);
assert!((BrainOperation::List {
category: None,
limit: 10,
}.ruv_reward() - 0.1).abs() < f64::EPSILON);
}
#[test]
fn test_brain_operation_names() {
assert_eq!(BrainOperation::Search { query: "q".into(), limit: 5 }.name(), "search");
assert_eq!(BrainOperation::Share {
title: "t".into(), content: "c".into(),
category: "cat".into(), tags: vec![],
}.name(), "share");
assert_eq!(BrainOperation::Vote {
memory_id: "m".into(), direction: "up".into(),
}.name(), "vote");
assert_eq!(BrainOperation::Status.name(), "status");
assert_eq!(BrainOperation::List { category: None, limit: 5 }.name(), "list");
assert_eq!(BrainOperation::LoraLatest.name(), "lora_latest");
}
#[test]
fn test_brain_search_request() {
let mut bridge = BrainBridge::new("ws://localhost:8080/relay", "node-abc");
let request = bridge.brain_search("auth patterns", 10);
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["type"], "brain_request");
assert_eq!(parsed["relay_url"], "ws://localhost:8080/relay");
assert_eq!(parsed["node_identity"], "node-abc");
assert_eq!(parsed["operation_name"], "search");
assert_eq!(parsed["ruv_reward"], 0.5);
}
#[test]
fn test_brain_share_request() {
let mut bridge = BrainBridge::new("ws://relay:8080", "node-xyz");
let request = bridge.brain_share(
"Auth Pattern",
"JWT with refresh tokens",
"security",
r#"["jwt", "auth"]"#,
);
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["type"], "brain_request");
assert_eq!(parsed["operation_name"], "share");
assert_eq!(parsed["ruv_reward"], 5.0);
let op = &parsed["operation"];
assert_eq!(op["Share"]["title"], "Auth Pattern");
assert_eq!(op["Share"]["content"], "JWT with refresh tokens");
assert_eq!(op["Share"]["category"], "security");
}
#[test]
fn test_brain_vote_validates_direction() {
let mut bridge = BrainBridge::new("ws://relay:8080", "node-1");
// Valid directions
let request = bridge.brain_vote("mem-1", "up");
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["Vote"]["direction"], "up");
let request = bridge.brain_vote("mem-2", "down");
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["Vote"]["direction"], "down");
// Invalid direction defaults to "up"
let request = bridge.brain_vote("mem-3", "sideways");
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["Vote"]["direction"], "up");
}
#[test]
fn test_brain_list_empty_category() {
let mut bridge = BrainBridge::new("ws://relay:8080", "node-1");
let request = bridge.brain_list("", 20);
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["List"]["category"], serde_json::Value::Null);
let request = bridge.brain_list("security", 20);
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["List"]["category"], "security");
}
#[test]
fn test_brain_search_caps_limit() {
let mut bridge = BrainBridge::new("ws://relay:8080", "node-1");
let request = bridge.brain_search("test", 500);
let parsed: serde_json::Value = serde_json::from_str(&request).unwrap();
assert_eq!(parsed["operation"]["Search"]["limit"], 100);
}
#[test]
fn test_brain_stats() {
let bridge = BrainBridge::new("ws://relay:8080", "node-1");
let stats = bridge.get_stats();
let parsed: serde_json::Value = serde_json::from_str(&stats).unwrap();
assert_eq!(parsed["ruv_earned"], 0.0);
assert_eq!(parsed["operations_count"], 0);
}
}

View file

@ -13,24 +13,33 @@ use uuid::Uuid;
pub mod qdag;
/// Contribution curve for reward calculation
/// Contribution curve for reward calculation.
///
/// The curve provides a multiplier for early adopters that decays
/// toward the floor as the network grows. The genesis multiplier
/// is capped at 5x (reduced from 10x for sustainability), and
/// a floor of 0.5x ensures contributors always earn something
/// even when the network is mature.
pub struct ContributionCurve;
impl ContributionCurve {
/// Maximum multiplier for genesis contributors
const MAX_BONUS: f32 = 10.0;
/// Maximum multiplier for genesis contributors (reduced from 10.0 for sustainability)
const MAX_BONUS: f32 = 5.0;
/// Floor multiplier -- even at massive scale, you still earn 0.5x
const FLOOR_MULTIPLIER: f32 = 0.5;
/// Decay constant in CPU-hours (half-life of bonus)
const DECAY_CONSTANT: f64 = 1_000_000.0;
/// Calculate current multiplier based on network compute
/// Calculate current multiplier based on network compute.
///
/// Formula: multiplier = 1 + (MAX_BONUS - 1) * e^(-network_compute / DECAY_CONSTANT)
/// Formula: multiplier = FLOOR + (MAX_BONUS - FLOOR) * e^(-network_compute / DECAY_CONSTANT)
///
/// Returns a value between 1.0 (baseline) and MAX_BONUS (genesis)
/// Returns a value between FLOOR_MULTIPLIER (mature network) and MAX_BONUS (genesis).
pub fn current_multiplier(network_compute_hours: f64) -> f32 {
let decay = (-network_compute_hours / Self::DECAY_CONSTANT).exp();
1.0 + (Self::MAX_BONUS - 1.0) * decay as f32
Self::FLOOR_MULTIPLIER + (Self::MAX_BONUS - Self::FLOOR_MULTIPLIER) * decay as f32
}
/// Calculate rewards with multiplier applied
@ -39,15 +48,32 @@ impl ContributionCurve {
(base_reward as f32 * multiplier) as u64
}
/// Get multiplier tiers for display
/// Calculate brain-specific reward with the contribution curve applied.
///
/// Brain rewards use the same decay curve but are further scaled by
/// a reputation multiplier. This bridges the brain_rewards module
/// with the contribution curve.
pub fn calculate_brain_reward(
base_reward: u64,
network_compute_hours: f64,
reputation_multiplier: f32,
) -> u64 {
let curve_multiplier = Self::current_multiplier(network_compute_hours);
let combined = curve_multiplier * reputation_multiplier;
(base_reward as f32 * combined) as u64
}
/// Get multiplier tiers for display.
///
/// Updated to reflect the reduced MAX_BONUS of 5.0 and floor of 0.5.
pub fn get_tiers() -> Vec<(f64, f32)> {
vec![
(0.0, 10.0),
(100_000.0, 9.1),
(500_000.0, 6.1),
(1_000_000.0, 4.0),
(5_000_000.0, 1.4),
(10_000_000.0, 1.0),
(0.0, 5.0),
(100_000.0, 4.59),
(500_000.0, 3.28),
(1_000_000.0, 2.16),
(5_000_000.0, 0.53),
(10_000_000.0, 0.50),
]
}
}
@ -286,17 +312,41 @@ mod tests {
#[test]
fn test_contribution_curve() {
// Genesis (0 hours) should give max multiplier
// Genesis (0 hours) should give max multiplier (5.0)
let mult = ContributionCurve::current_multiplier(0.0);
assert!((mult - 10.0).abs() < 0.01);
assert!((mult - 5.0).abs() < 0.01);
// At decay constant, should be around 4.3x
// At decay constant, should be around 2.16
// FLOOR + (MAX - FLOOR) * e^(-1) = 0.5 + 4.5 * 0.368 = 2.16
let mult = ContributionCurve::current_multiplier(1_000_000.0);
assert!(mult > 3.5 && mult < 4.5);
assert!(mult > 1.8 && mult < 2.5);
// At high compute, should approach 1.0
// At high compute, should approach FLOOR_MULTIPLIER (0.5)
let mult = ContributionCurve::current_multiplier(10_000_000.0);
assert!(mult < 1.1);
assert!(mult >= 0.5);
assert!(mult < 0.6);
}
#[test]
fn test_contribution_curve_floor() {
// Even at extremely high compute, multiplier never goes below 0.5
let mult = ContributionCurve::current_multiplier(100_000_000.0);
assert!(mult >= 0.5);
}
#[test]
fn test_brain_reward_calculation() {
// At genesis with Bronze (1.0x rep): 100 * 5.0 * 1.0 = 500
let reward = ContributionCurve::calculate_brain_reward(100, 0.0, 1.0);
assert_eq!(reward, 500);
// At genesis with Newcomer (0.5x rep): 100 * 5.0 * 0.5 = 250
let reward = ContributionCurve::calculate_brain_reward(100, 0.0, 0.5);
assert_eq!(reward, 250);
// At high compute with Platinum (1.5x rep): 100 * 0.5 * 1.5 = 75
let reward = ContributionCurve::calculate_brain_reward(100, 100_000_000.0, 1.5);
assert_eq!(reward, 75);
}
// Tests requiring WASM environment (UUID with js feature)

View file

@ -570,6 +570,90 @@ impl ComputeAMM {
history.iter().rev().take(limit).cloned().collect()
}
// ========== Accessibility Helpers ==========
/// Estimate the rUv cost for a given number of compute-seconds.
///
/// Returns a human-readable cost estimate without executing a swap.
/// This helps newcomers understand pricing before committing.
pub fn estimate_compute_cost(&self, seconds: u64) -> u64 {
if seconds == 0 {
return 0;
}
let reserve_ruv = *self.reserve_ruv.read().unwrap();
let reserve_compute = *self.reserve_compute.read().unwrap();
let k = *self.k_invariant.read().unwrap();
if seconds as u128 >= reserve_compute as u128 {
return u64::MAX;
}
// From constant product: new_compute = reserve_compute - seconds
// new_ruv = k / new_compute
// ruv_needed = new_ruv - reserve_ruv (before fees)
let new_compute = (reserve_compute as u128).saturating_sub(seconds as u128);
if new_compute == 0 {
return u64::MAX;
}
let new_ruv = k / new_compute;
let ruv_before_fee = (new_ruv as u64).saturating_sub(reserve_ruv);
// Account for fee: actual_in = ruv_before_fee / (1 - fee_rate)
let fee_rate = self.dynamic_fee() as f64;
if fee_rate >= 1.0 {
return u64::MAX;
}
(ruv_before_fee as f64 / (1.0 - fee_rate)).ceil() as u64
}
/// Get price history from swap events.
///
/// Returns a list of (timestamp, price_at_that_time) tuples for the
/// last `last_n` swaps. Useful for price transparency dashboards.
pub fn get_price_history(&self, last_n: usize) -> Vec<(u64, f64)> {
let history = self.swap_history.read().unwrap();
history.iter()
.rev()
.take(last_n)
.map(|event| {
let price = if event.amount_out > 0 {
match event.input_type {
SwapType::RuvForCompute => event.amount_in as f64 / event.amount_out as f64,
SwapType::ComputeForRuv => event.amount_out as f64 / event.amount_in as f64,
}
} else {
0.0
};
(event.timestamp, price)
})
.collect()
}
/// Get a human-readable pool status summary.
///
/// Returns a formatted string suitable for display to end users
/// who may not understand AMM mechanics.
pub fn get_readable_status(&self) -> String {
let price = self.get_price();
let utilization = self.get_utilization();
let fee = self.dynamic_fee();
let status = if utilization < 0.3 {
"Low demand - prices are low"
} else if utilization < 0.7 {
"Moderate demand - normal pricing"
} else {
"High demand - prices are elevated"
};
format!(
"Current price: {:.4} rUv per compute-second | Fee: {:.1}% | Status: {}",
price,
fee * 100.0,
status,
)
}
/// Calculate price impact for a swap
pub fn calculate_price_impact(&self, ruv_in: u64) -> f64 {
let current_price = self.get_price();
@ -661,4 +745,47 @@ mod tests {
assert!(ruv > 0);
assert!(compute > 0);
}
#[test]
fn test_estimate_compute_cost() {
let amm = ComputeAMM::new(1_000_000, 1_000_000).unwrap();
// Cost for 0 seconds should be 0
assert_eq!(amm.estimate_compute_cost(0), 0);
// Cost for small amount should be reasonable (close to 1:1 at balanced pool)
let cost = amm.estimate_compute_cost(1000);
assert!(cost > 0);
assert!(cost > 1000);
assert!(cost < 1100);
// Cost for too much compute should return MAX
let cost = amm.estimate_compute_cost(2_000_000);
assert_eq!(cost, u64::MAX);
}
#[test]
fn test_get_price_history_empty() {
let amm = ComputeAMM::new(1_000_000, 1_000_000).unwrap();
let history = amm.get_price_history(10);
assert!(history.is_empty());
}
#[test]
fn test_get_price_history_after_swap() {
let amm = ComputeAMM::new(1_000_000, 1_000_000).unwrap();
let _ = amm.swap_ruv_for_compute(10_000, "test");
let history = amm.get_price_history(10);
assert_eq!(history.len(), 1);
assert!(history[0].1 > 0.0);
}
#[test]
fn test_get_readable_status() {
let amm = ComputeAMM::new(1_000_000, 1_000_000).unwrap();
let status = amm.get_readable_status();
assert!(status.contains("rUv per compute-second"));
assert!(status.contains("Fee:"));
assert!(status.contains("Low demand"));
}
}

View file

@ -0,0 +1,566 @@
//! # Brain Operation Rewards
//!
//! Defines the rUv reward schedule for brain-related operations.
//! Designed for accessibility (free tier for reads) and sustainability
//! (rewards decrease as the network matures via a halving schedule).
//!
//! ## Design Principles
//!
//! 1. **Free to read, earn to write**: No barriers to consuming knowledge;
//! contributions earn rUv.
//! 2. **Halving schedule**: Rewards halve every 100K brain operations,
//! similar to Bitcoin halvings but at the application layer.
//! 3. **Reputation floor**: Even newcomers earn (at 0.5x), just less
//! than established nodes.
//! 4. **Sustainable cap**: Max rUv minted per epoch is bounded by the
//! protocol budget.
//! 5. **Transparency**: All pricing and rewards visible via helper methods.
//!
//! ## Reward Table
//!
//! ```text
//! ┌──────────────┬──────────┬───────────────────────────────────┐
//! │ Operation │ Cost │ Base Reward │
//! ├──────────────┼──────────┼───────────────────────────────────┤
//! │ Search │ FREE │ 0 rUv │
//! │ Status │ FREE │ 0 rUv │
//! │ List │ FREE │ 0 rUv │
//! │ Share │ 0 rUv │ 2 rUv (if quality > 0.5) │
//! │ Vote │ 0 rUv │ 0.1 rUv per vote │
//! │ Embedding │ 0 rUv │ 1 rUv per embedding │
//! │ LoRA Train │ 0 rUv │ 5 rUv per accepted gradient │
//! │ WASM Compute│ 0 rUv │ compute-time based │
//! └──────────────┴──────────┴───────────────────────────────────┘
//! ```
use serde::{Serialize, Deserialize};
use super::reputation::ReputationTier;
/// Number of brain operations between each halving
pub const HALVING_INTERVAL: u64 = 100_000;
/// Maximum halvings before reward floor kicks in
pub const MAX_HALVINGS: u32 = 10;
/// Default epoch budget in rUv (caps total minting per epoch)
pub const DEFAULT_EPOCH_BUDGET: u64 = 1_000_000;
/// Floor reward multiplier -- rewards never go below this fraction
/// of the base amount, even after many halvings
pub const FLOOR_MULTIPLIER: f64 = 0.01;
/// Types of brain operations with their associated parameters.
///
/// Free-to-read, earn-to-write model:
/// - Search, Status, List: FREE (no cost, no reward)
/// - Share: costs 0 rUv, earns 2 rUv if quality > 0.5
/// - Vote: costs 0 rUv, earns fractional rUv per vote
/// - Embedding: costs 0 rUv, earns 1 rUv per embedding
/// - LoraTraining: costs 0 rUv, earns 5 rUv per accepted gradient
/// - WasmCompute: costs 0 rUv, earns based on compute time
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum BrainOpType {
/// Search the brain -- always free, no reward
Search,
/// Check brain status -- always free, no reward
Status,
/// List brain contents -- always free, no reward
List,
/// Share knowledge with the brain (quality 0.0-1.0)
Share { quality: f64 },
/// Vote on brain content
Vote,
/// Generate an embedding
Embedding,
/// LoRA training contribution
LoraTraining,
/// WASM compute contribution
WasmCompute { seconds: u64 },
}
/// Result of a brain reward calculation.
///
/// `cost` is how much the user pays (0 for free-tier ops).
/// `reward` is how much the user earns.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct BrainRewardResult {
/// Cost to the user in rUv (0 for free operations)
pub cost: u64,
/// Reward earned in rUv
pub reward: u64,
/// Whether this operation is in the free tier
pub is_free_tier: bool,
/// The halving multiplier applied (1.0 at genesis, 0.5 after first halving, etc.)
pub halving_multiplier: f64,
/// The reputation multiplier applied
pub reputation_multiplier: f64,
}
/// Brain reward engine that tracks epochs and budgets.
///
/// Implements a halving schedule where rewards decrease as the network
/// matures, combined with per-epoch budgets to cap total minting.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BrainRewards {
/// Current epoch number (rewards decrease over epochs)
epoch: u64,
/// Total brain operations across all epochs
total_ops: u64,
/// Operations in the current epoch
epoch_ops: u64,
/// Budget remaining in the current epoch (rUv)
epoch_budget: u64,
/// Maximum budget per epoch
max_epoch_budget: u64,
/// Total rUv minted through brain rewards (lifetime)
total_minted: u64,
}
impl BrainRewards {
/// Create a new brain rewards engine with default budget.
pub fn new() -> Self {
Self {
epoch: 0,
total_ops: 0,
epoch_ops: 0,
epoch_budget: DEFAULT_EPOCH_BUDGET,
max_epoch_budget: DEFAULT_EPOCH_BUDGET,
total_minted: 0,
}
}
/// Create a brain rewards engine with a custom epoch budget.
pub fn with_budget(max_epoch_budget: u64) -> Self {
Self {
epoch: 0,
total_ops: 0,
epoch_ops: 0,
epoch_budget: max_epoch_budget,
max_epoch_budget,
total_minted: 0,
}
}
/// Get the current epoch number.
pub fn epoch(&self) -> u64 {
self.epoch
}
/// Get total brain operations processed (lifetime).
pub fn total_ops(&self) -> u64 {
self.total_ops
}
/// Get operations processed in the current epoch.
pub fn epoch_ops(&self) -> u64 {
self.epoch_ops
}
/// Get the remaining budget for this epoch.
pub fn epoch_budget(&self) -> u64 {
self.epoch_budget
}
/// Get total rUv minted through brain rewards (lifetime).
pub fn total_minted(&self) -> u64 {
self.total_minted
}
/// Calculate the halving multiplier based on total network operations.
///
/// Rewards halve every `HALVING_INTERVAL` operations. After
/// `MAX_HALVINGS`, the multiplier is floored at `FLOOR_MULTIPLIER`.
pub fn halving_multiplier(&self) -> f64 {
let halvings = (self.total_ops / HALVING_INTERVAL) as u32;
if halvings >= MAX_HALVINGS {
return FLOOR_MULTIPLIER;
}
let mult = 0.5_f64.powi(halvings as i32);
mult.max(FLOOR_MULTIPLIER)
}
/// Calculate the reputation multiplier for a given tier.
///
/// This extends the existing tier multipliers to include the
/// Newcomer tier at 0.5x.
pub fn reputation_multiplier(tier: &ReputationTier) -> f64 {
match tier {
ReputationTier::Newcomer => 0.5,
ReputationTier::Bronze => 1.0,
ReputationTier::Silver => 1.1,
ReputationTier::Gold => 1.25,
ReputationTier::Platinum => 1.5,
}
}
/// Calculate reward for a brain operation.
///
/// Returns a `BrainRewardResult` with cost and reward.
/// Free-tier operations (Search, Status, List) always cost 0 and earn 0.
/// Earn-tier operations cost 0 but earn rUv scaled by halving and reputation.
pub fn calculate(&self, op: &BrainOpType, reputation_tier: &ReputationTier) -> BrainRewardResult {
let halving = self.halving_multiplier();
let rep_mult = Self::reputation_multiplier(reputation_tier);
match op {
// Free tier: no cost, no reward
BrainOpType::Search | BrainOpType::Status | BrainOpType::List => {
BrainRewardResult {
cost: 0,
reward: 0,
is_free_tier: true,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
// Share: earn 2 rUv base if quality > 0.5, otherwise 0
BrainOpType::Share { quality } => {
let base_reward = if *quality > 0.5 { 2.0 } else { 0.0 };
let reward = (base_reward * halving * rep_mult) as u64;
BrainRewardResult {
cost: 0,
reward: reward.min(self.epoch_budget),
is_free_tier: false,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
// Vote: earn 0.1 rUv base (rounds to at least 1 if multiplied high enough)
BrainOpType::Vote => {
// Use fixed-point: 0.1 rUv base
// At genesis with Platinum: 0.1 * 1.0 * 1.5 = 0.15 -> rounds to 0
// We use a minimum of 1 rUv for non-zero results to avoid zero rewards
let raw = 0.1 * halving * rep_mult;
let reward = if raw >= 0.05 { (raw as u64).max(1) } else { 0 };
BrainRewardResult {
cost: 0,
reward: reward.min(self.epoch_budget),
is_free_tier: false,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
// Embedding: earn 1 rUv base
BrainOpType::Embedding => {
let reward = (1.0 * halving * rep_mult) as u64;
BrainRewardResult {
cost: 0,
reward: reward.min(self.epoch_budget),
is_free_tier: false,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
// LoRA training: earn 5 rUv base
BrainOpType::LoraTraining => {
let reward = (5.0 * halving * rep_mult) as u64;
BrainRewardResult {
cost: 0,
reward: reward.min(self.epoch_budget),
is_free_tier: false,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
// WASM compute: earn 1 rUv per second base
BrainOpType::WasmCompute { seconds } => {
let base = *seconds as f64;
let reward = (base * halving * rep_mult) as u64;
BrainRewardResult {
cost: 0,
reward: reward.min(self.epoch_budget),
is_free_tier: false,
halving_multiplier: halving,
reputation_multiplier: rep_mult,
}
}
}
}
/// Record a brain operation and return the reward result.
///
/// This both calculates the reward and updates internal state
/// (operation counters, budget, minted totals).
pub fn record_operation(&mut self, op: &BrainOpType, reputation_tier: &ReputationTier) -> BrainRewardResult {
let result = self.calculate(op, reputation_tier);
// Update counters
self.total_ops += 1;
self.epoch_ops += 1;
// Deduct from epoch budget
if result.reward > 0 && self.epoch_budget >= result.reward {
self.epoch_budget -= result.reward;
self.total_minted += result.reward;
}
result
}
/// Advance to the next epoch, resetting the epoch budget.
///
/// The budget resets to the max epoch budget, and the epoch
/// operation counter resets. Total ops and total minted persist.
pub fn advance_epoch(&mut self) {
self.epoch += 1;
self.epoch_ops = 0;
self.epoch_budget = self.max_epoch_budget;
}
/// Get a human-readable summary of the reward schedule as JSON.
///
/// Useful for dashboards and transparency displays.
pub fn get_schedule_summary(&self) -> String {
let halving = self.halving_multiplier();
let halvings_completed = (self.total_ops / HALVING_INTERVAL).min(MAX_HALVINGS as u64);
let next_halving_at = (self.total_ops / HALVING_INTERVAL + 1) * HALVING_INTERVAL;
let ops_until_next = next_halving_at - self.total_ops;
let share_reward = format!("{:.1} rUv (quality > 0.5)", 2.0 * halving);
let vote_reward = format!("{:.2} rUv", 0.1 * halving);
let embedding_reward = format!("{:.1} rUv", 1.0 * halving);
let lora_reward = format!("{:.1} rUv", 5.0 * halving);
let wasm_reward = format!("{:.1} rUv/second", 1.0 * halving);
let summary = serde_json::json!({
"epoch": self.epoch,
"total_operations": self.total_ops,
"epoch_operations": self.epoch_ops,
"epoch_budget_remaining": self.epoch_budget,
"max_epoch_budget": self.max_epoch_budget,
"total_minted": self.total_minted,
"current_halving_multiplier": halving,
"halvings_completed": halvings_completed,
"next_halving_at": next_halving_at,
"operations_until_next_halving": ops_until_next,
"free_tier_operations": ["Search", "Status", "List"],
"earn_tier_operations": {
"Share": share_reward,
"Vote": vote_reward,
"Embedding": embedding_reward,
"LoraTraining": lora_reward,
"WasmCompute": wasm_reward
},
"reputation_multipliers": {
"Newcomer": 0.5,
"Bronze": 1.0,
"Silver": 1.1,
"Gold": 1.25,
"Platinum": 1.5
}
});
serde_json::to_string_pretty(&summary).unwrap_or_else(|_| "{}".to_string())
}
}
impl Default for BrainRewards {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_free_tier_operations() {
let rewards = BrainRewards::new();
// Search is free
let result = rewards.calculate(&BrainOpType::Search, &ReputationTier::Newcomer);
assert_eq!(result.cost, 0);
assert_eq!(result.reward, 0);
assert!(result.is_free_tier);
// Status is free
let result = rewards.calculate(&BrainOpType::Status, &ReputationTier::Bronze);
assert_eq!(result.cost, 0);
assert_eq!(result.reward, 0);
assert!(result.is_free_tier);
// List is free
let result = rewards.calculate(&BrainOpType::List, &ReputationTier::Platinum);
assert_eq!(result.cost, 0);
assert_eq!(result.reward, 0);
assert!(result.is_free_tier);
}
#[test]
fn test_share_reward() {
let rewards = BrainRewards::new();
// High quality share earns reward
let result = rewards.calculate(
&BrainOpType::Share { quality: 0.8 },
&ReputationTier::Bronze,
);
assert_eq!(result.cost, 0);
assert_eq!(result.reward, 2); // 2 * 1.0 halving * 1.0 rep = 2
assert!(!result.is_free_tier);
// Low quality share earns nothing
let result = rewards.calculate(
&BrainOpType::Share { quality: 0.3 },
&ReputationTier::Bronze,
);
assert_eq!(result.cost, 0);
assert_eq!(result.reward, 0);
}
#[test]
fn test_vote_reward() {
let rewards = BrainRewards::new();
// Vote at genesis with Bronze (1.0x rep)
let result = rewards.calculate(&BrainOpType::Vote, &ReputationTier::Bronze);
assert_eq!(result.cost, 0);
// 0.1 * 1.0 * 1.0 = 0.1 -> rounds up to minimum of 1
assert_eq!(result.reward, 1);
}
#[test]
fn test_embedding_reward() {
let rewards = BrainRewards::new();
let result = rewards.calculate(&BrainOpType::Embedding, &ReputationTier::Gold);
assert_eq!(result.cost, 0);
// 1.0 * 1.0 halving * 1.25 rep = 1.25 -> 1
assert_eq!(result.reward, 1);
}
#[test]
fn test_lora_training_reward() {
let rewards = BrainRewards::new();
let result = rewards.calculate(&BrainOpType::LoraTraining, &ReputationTier::Platinum);
assert_eq!(result.cost, 0);
// 5.0 * 1.0 halving * 1.5 rep = 7.5 -> 7
assert_eq!(result.reward, 7);
}
#[test]
fn test_wasm_compute_reward() {
let rewards = BrainRewards::new();
let result = rewards.calculate(
&BrainOpType::WasmCompute { seconds: 10 },
&ReputationTier::Silver,
);
assert_eq!(result.cost, 0);
// 10 * 1.0 halving * 1.1 rep = 11.0 -> 11
assert_eq!(result.reward, 11);
}
#[test]
fn test_newcomer_gets_half_rewards() {
let rewards = BrainRewards::new();
let newcomer = rewards.calculate(&BrainOpType::LoraTraining, &ReputationTier::Newcomer);
let bronze = rewards.calculate(&BrainOpType::LoraTraining, &ReputationTier::Bronze);
// Newcomer: 5 * 1.0 * 0.5 = 2.5 -> 2
assert_eq!(newcomer.reward, 2);
// Bronze: 5 * 1.0 * 1.0 = 5
assert_eq!(bronze.reward, 5);
// Newcomer earns less but still earns
assert!(newcomer.reward > 0);
assert!(newcomer.reward < bronze.reward);
}
#[test]
fn test_halving_schedule() {
let mut rewards = BrainRewards::new();
// At genesis, multiplier is 1.0
assert!((rewards.halving_multiplier() - 1.0).abs() < 0.001);
// Simulate 100K operations
rewards.total_ops = HALVING_INTERVAL;
assert!((rewards.halving_multiplier() - 0.5).abs() < 0.001);
// After 200K operations
rewards.total_ops = 2 * HALVING_INTERVAL;
assert!((rewards.halving_multiplier() - 0.25).abs() < 0.001);
// After max halvings, hits floor
rewards.total_ops = (MAX_HALVINGS as u64 + 5) * HALVING_INTERVAL;
assert!((rewards.halving_multiplier() - FLOOR_MULTIPLIER).abs() < 0.001);
}
#[test]
fn test_epoch_budget_cap() {
let mut rewards = BrainRewards::with_budget(10);
// Record a LoRA training that would earn 5
let result = rewards.record_operation(&BrainOpType::LoraTraining, &ReputationTier::Bronze);
assert_eq!(result.reward, 5);
assert_eq!(rewards.epoch_budget(), 5);
// Record another -- still fits in budget
let result = rewards.record_operation(&BrainOpType::LoraTraining, &ReputationTier::Bronze);
assert_eq!(result.reward, 5);
assert_eq!(rewards.epoch_budget(), 0);
// Budget is exhausted, rewards are capped to 0
let result = rewards.calculate(&BrainOpType::LoraTraining, &ReputationTier::Bronze);
// Reward is min(5, 0) = 0 because budget is 0
assert_eq!(result.reward, 0);
}
#[test]
fn test_advance_epoch_resets_budget() {
let mut rewards = BrainRewards::with_budget(100);
// Spend some budget
rewards.record_operation(&BrainOpType::LoraTraining, &ReputationTier::Bronze);
assert!(rewards.epoch_budget() < 100);
assert_eq!(rewards.epoch_ops(), 1);
// Advance epoch
rewards.advance_epoch();
assert_eq!(rewards.epoch_budget(), 100);
assert_eq!(rewards.epoch_ops(), 0);
assert_eq!(rewards.epoch(), 1);
// Total ops persist
assert_eq!(rewards.total_ops(), 1);
}
#[test]
fn test_record_operation_updates_counters() {
let mut rewards = BrainRewards::new();
rewards.record_operation(&BrainOpType::Search, &ReputationTier::Bronze);
assert_eq!(rewards.total_ops(), 1);
assert_eq!(rewards.epoch_ops(), 1);
// Search earns 0, so total_minted stays 0
assert_eq!(rewards.total_minted(), 0);
rewards.record_operation(&BrainOpType::Embedding, &ReputationTier::Bronze);
assert_eq!(rewards.total_ops(), 2);
assert_eq!(rewards.total_minted(), 1); // 1 rUv for embedding
}
#[test]
fn test_schedule_summary_is_valid_json() {
let rewards = BrainRewards::new();
let summary = rewards.get_schedule_summary();
let parsed: serde_json::Value = serde_json::from_str(&summary).unwrap();
assert!(parsed.is_object());
assert!(parsed["epoch"].is_number());
assert!(parsed["free_tier_operations"].is_array());
}
#[test]
fn test_default_impl() {
let rewards = BrainRewards::default();
assert_eq!(rewards.epoch(), 0);
assert_eq!(rewards.total_ops(), 0);
assert_eq!(rewards.epoch_budget(), DEFAULT_EPOCH_BUDGET);
}
}

View file

@ -8,14 +8,23 @@
//! - x * y = k invariant
//! - Dynamic fee based on utilization
//! - Liquidity provision
//! - Accessibility helpers for cost estimation and price transparency
//!
//! - **Reputation**: Bonding curves for trust and pricing
//! - Reputation-weighted discounts
//! - Superlinear task allocation priority
//! - Stake requirements
//! - Newcomer tier for zero-barrier onboarding
//!
//! - **Brain Rewards**: Brain-specific reward schedule
//! - Free-to-read, earn-to-write model
//! - Halving schedule for sustainability
//! - Epoch-bounded minting budget
pub mod amm;
pub mod reputation;
pub mod brain_rewards;
pub use amm::*;
pub use reputation::*;
pub use brain_rewards::*;

View file

@ -57,7 +57,9 @@ pub const MAX_DISCOUNT: f32 = 0.20;
/// Reputation tier thresholds
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ReputationTier {
/// New or low reputation (0-25)
/// Brand new participant (0-10) -- free reads, low-barrier entry
Newcomer,
/// Low reputation (10-25)
Bronze,
/// Moderate reputation (25-50)
Silver,
@ -68,19 +70,26 @@ pub enum ReputationTier {
}
impl ReputationTier {
/// Get tier from reputation score
/// Get tier from reputation score.
///
/// The Newcomer tier (0-10) provides zero-barrier onboarding:
/// - Free read access (no stake required for searches)
/// - 0 minimum stake for read-only operations
/// - 10 rUv minimum stake only for write operations
pub fn from_score(reputation: f32) -> Self {
match reputation {
r if r >= 75.0 => ReputationTier::Platinum,
r if r >= 50.0 => ReputationTier::Gold,
r if r >= 25.0 => ReputationTier::Silver,
_ => ReputationTier::Bronze,
r if r >= 10.0 => ReputationTier::Bronze,
_ => ReputationTier::Newcomer,
}
}
/// Get tier name
pub fn name(&self) -> &str {
match self {
ReputationTier::Newcomer => "Newcomer",
ReputationTier::Bronze => "Bronze",
ReputationTier::Silver => "Silver",
ReputationTier::Gold => "Gold",
@ -88,15 +97,97 @@ impl ReputationTier {
}
}
/// Get tier multiplier for rewards
/// Get tier multiplier for rewards.
///
/// Newcomers earn at 0.5x -- still earning, just less than established nodes.
/// This solves the cold-start problem: you can earn from day one.
pub fn reward_multiplier(&self) -> f32 {
match self {
ReputationTier::Newcomer => 0.5,
ReputationTier::Bronze => 1.0,
ReputationTier::Silver => 1.1,
ReputationTier::Gold => 1.25,
ReputationTier::Platinum => 1.5,
}
}
/// Get minimum stake required for this tier (in rUv).
///
/// Newcomers need 0 stake for reads, 10 for writes.
/// Higher tiers require more stake to maintain.
pub fn min_stake(&self) -> u64 {
match self {
ReputationTier::Newcomer => 0,
ReputationTier::Bronze => 10,
ReputationTier::Silver => 50,
ReputationTier::Gold => 200,
ReputationTier::Platinum => 500,
}
}
/// Get a JSON description of what each tier provides.
///
/// Useful for onboarding UIs and transparency dashboards.
pub fn get_tier_requirements() -> String {
let tiers = serde_json::json!({
"Newcomer": {
"reputation_range": "0-10",
"min_stake": 0,
"reward_multiplier": 0.5,
"benefits": [
"Free read access (search, status, list)",
"Earn rUv by contributing (at 0.5x rate)",
"No stake required for read operations",
"10 rUv stake required for write operations"
]
},
"Bronze": {
"reputation_range": "10-25",
"min_stake": 10,
"reward_multiplier": 1.0,
"benefits": [
"Full read access",
"Standard reward rate (1.0x)",
"Basic task allocation priority"
]
},
"Silver": {
"reputation_range": "25-50",
"min_stake": 50,
"reward_multiplier": 1.1,
"benefits": [
"Full read/write access",
"10% reward bonus",
"Moderate task allocation priority",
"5% compute discount"
]
},
"Gold": {
"reputation_range": "50-75",
"min_stake": 200,
"reward_multiplier": 1.25,
"benefits": [
"Full access",
"25% reward bonus",
"High task allocation priority",
"10% compute discount"
]
},
"Platinum": {
"reputation_range": "75-100",
"min_stake": 500,
"reward_multiplier": 1.5,
"benefits": [
"Full access",
"50% reward bonus",
"Maximum task allocation priority",
"20% compute discount",
"Governance voting weight"
]
}
});
serde_json::to_string_pretty(&tiers).unwrap_or_else(|_| "{}".to_string())
}
}
/// Reputation bonding curve configuration
@ -297,6 +388,7 @@ impl ReputationCurve {
#[wasm_bindgen(js_name = getTierDistribution)]
pub fn get_tier_distribution(&self) -> String {
let reps = self.reputations.read().unwrap();
let mut newcomer = 0;
let mut bronze = 0;
let mut silver = 0;
let mut gold = 0;
@ -304,6 +396,7 @@ impl ReputationCurve {
for rep in reps.values() {
match rep.tier {
ReputationTier::Newcomer => newcomer += 1,
ReputationTier::Bronze => bronze += 1,
ReputationTier::Silver => silver += 1,
ReputationTier::Gold => gold += 1,
@ -312,6 +405,7 @@ impl ReputationCurve {
}
let dist = serde_json::json!({
"newcomer": newcomer,
"bronze": bronze,
"silver": silver,
"gold": gold,
@ -565,6 +659,7 @@ mod tests {
#[test]
fn test_reputation_tiers() {
assert_eq!(ReputationTier::from_score(5.0), ReputationTier::Newcomer);
assert_eq!(ReputationTier::from_score(10.0), ReputationTier::Bronze);
assert_eq!(ReputationTier::from_score(30.0), ReputationTier::Silver);
assert_eq!(ReputationTier::from_score(60.0), ReputationTier::Gold);
@ -588,9 +683,31 @@ mod tests {
fn test_reward_multiplier() {
let curve = ReputationCurve::new();
assert_eq!(curve.get_reward_multiplier(5.0), 0.5); // Newcomer
assert_eq!(curve.get_reward_multiplier(10.0), 1.0); // Bronze
assert_eq!(curve.get_reward_multiplier(30.0), 1.1); // Silver
assert_eq!(curve.get_reward_multiplier(60.0), 1.25); // Gold
assert_eq!(curve.get_reward_multiplier(90.0), 1.5); // Platinum
}
#[test]
fn test_newcomer_tier() {
// Newcomers (0-10 rep) get 0.5x rewards and 0 min stake
let tier = ReputationTier::from_score(0.0);
assert_eq!(tier, ReputationTier::Newcomer);
assert_eq!(tier.reward_multiplier(), 0.5);
assert_eq!(tier.min_stake(), 0);
assert_eq!(tier.name(), "Newcomer");
}
#[test]
fn test_tier_requirements_is_valid_json() {
let json = ReputationTier::get_tier_requirements();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["Newcomer"].is_object());
assert!(parsed["Bronze"].is_object());
assert!(parsed["Silver"].is_object());
assert!(parsed["Gold"].is_object());
assert!(parsed["Platinum"].is_object());
}
}

View file

@ -56,6 +56,7 @@ pub mod swarm;
pub mod capabilities;
pub mod compute;
pub mod ai;
pub mod economics;
use identity::WasmNodeIdentity;
use learning::NetworkLearning;

848
examples/rvf/manifest.json Normal file
View file

@ -0,0 +1,848 @@
{
"version": "0.2.1",
"updated": "2026-02-28T06:56:46Z",
"base_url": "https://storage.googleapis.com/ruvector-examples/v0.2.1",
"total_size": "15.6 MB",
"total_size_bytes": 16367350,
"count": 46,
"examples": [
{
"name": "access_control",
"file": "access_control.rvf",
"size": 78444,
"size_human": "76.6 KB",
"sha256": "d3ba28d5a28bbe3b0167f0cbb08d4bea1fcb741ae57e5da957b3760043dabde7",
"description": "Access Control",
"category": "security",
"tags": [
"security",
"access"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "agent_handoff_a",
"file": "agent_handoff_a.rvf",
"size": 31404,
"size_human": "30.7 KB",
"sha256": "6bea24466e08219f93198de047e532194c21b2e3ecb23f7629518ec1bae03994",
"description": "Agent Handoff A",
"category": "network",
"tags": [
"network",
"agent"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "agent_handoff_b",
"file": "agent_handoff_b.rvf",
"size": 10764,
"size_human": "10.5 KB",
"sha256": "61b682480d2fe2dae3be9980b36b1cdcab2fec34b0bf56d8cf8d8fcac9cb56ec",
"description": "Agent Handoff B",
"category": "network",
"tags": [
"network",
"agent"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "agent_memory",
"file": "agent_memory.rvf",
"size": 32118,
"size_human": "31.4 KB",
"sha256": "c6fccc3e6121ab6b849d1b28c78fbfeb5e1359606b36d5c44d0afa01f672961b",
"description": "Agent Memory",
"category": "ai",
"tags": [
"ai",
"agent"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "basic_store",
"file": "basic_store.rvf",
"size": 154844,
"size_human": "151.2 KB",
"sha256": "063f6bd4178baaf65f0d9e48e858985b4358b55c49152e3f08d53968a680b2f4",
"description": "Basic Store",
"category": "core",
"tags": [
"core",
"basic"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "browser_wasm",
"file": "browser_wasm.rvf",
"size": 13644,
"size_human": "13.3 KB",
"sha256": "16fdcc8e7d3f44ff51833c1a1ac7b70ee6320df8091a7ffa92fe5cdee8c408d1",
"description": "Browser Wasm",
"category": "compute",
"tags": [
"compute",
"browser"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "claude_code_appliance",
"file": "claude_code_appliance.rvf",
"size": 5260093,
"size_human": "5.0 MB",
"sha256": "82ac74d626e5cfe50c7c622e8d5e2c95a01a9654781337b2734a7fedb26dff53",
"description": "Claude Code Appliance",
"category": "integration",
"tags": [
"integration",
"claude"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "claude_code_appliance_v1",
"file": "claude_code_appliance_v1.rvf",
"size": 162,
"size_human": "162 B",
"sha256": "c78bcd5e82d8afa6d0757ac2d70cab05f67d5f5ddc5886548584dbbc42fc8382",
"description": "Claude Code Appliance V1",
"category": "integration",
"tags": [
"integration",
"claude"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "compacted",
"file": "compacted.rvf",
"size": 78257,
"size_human": "76.4 KB",
"sha256": "59260c0798b9618c01ca4932d169f18a4e3b4b944075e3716e6a5e862584518a",
"description": "Compacted",
"category": "core",
"tags": [
"core",
"compacted"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "dedup_detector",
"file": "dedup_detector.rvf",
"size": 156444,
"size_human": "152.8 KB",
"sha256": "e14c4e74170afaeb448f462b5a838f0ed06837632806de9d1a506156834adfcb",
"description": "Dedup Detector",
"category": "core",
"tags": [
"core",
"dedup"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "ebpf_accelerator",
"file": "ebpf_accelerator.rvf",
"size": 156514,
"size_human": "152.8 KB",
"sha256": "533c04b0753f5a7198b8a4f5188bb000bb795fa225499794df835d0523deab07",
"description": "Ebpf Accelerator",
"category": "compute",
"tags": [
"compute",
"ebpf"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "edge_iot",
"file": "edge_iot.rvf",
"size": 27644,
"size_human": "27.0 KB",
"sha256": "8d79abd3625fe7e662dc7660b15bb5b93e1e9f0dcc84da2c6c9a8670b8159248",
"description": "Edge Iot",
"category": "compute",
"tags": [
"compute",
"edge"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "embedding_cache",
"file": "embedding_cache.rvf",
"size": 772444,
"size_human": "754.3 KB",
"sha256": "6cf09ad8052cb13cb4074db56a01403c889a15dbec798ec9e25a764174fe3aff",
"description": "Embedding Cache",
"category": "core",
"tags": [
"core",
"embedding"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "experience_replay",
"file": "experience_replay.rvf",
"size": 26844,
"size_human": "26.2 KB",
"sha256": "4443d1f8aefd94e7253c797334182d55f406cbb9b9ba54b0831bd7e84df71a78",
"description": "Experience Replay",
"category": "ai",
"tags": [
"ai",
"experience"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "filtered_search",
"file": "filtered_search.rvf",
"size": 260444,
"size_human": "254.3 KB",
"sha256": "37309666570ca04e518ef66c26b48e9034ee5855e17104177d72d9901f658f80",
"description": "Filtered Search",
"category": "core",
"tags": [
"core",
"filtered"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "financial_signals",
"file": "financial_signals.rvf",
"size": 206844,
"size_human": "202.0 KB",
"sha256": "157d6a3df91191916486926c6a0312bd9f3cd0c8c8d4883ec33552683a075750",
"description": "Financial Signals",
"category": "industry",
"tags": [
"industry",
"financial"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "hyperbolic_taxonomy",
"file": "hyperbolic_taxonomy.rvf",
"size": 22884,
"size_human": "22.3 KB",
"sha256": "6ac67203c2b57c00d2aea13572f3c37af23a7bf0d8603ae8987a1544ee97a193",
"description": "Hyperbolic Taxonomy",
"category": "core",
"tags": [
"core",
"hyperbolic"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "legal_discovery",
"file": "legal_discovery.rvf",
"size": 924444,
"size_human": "902.8 KB",
"sha256": "830d353ab86f9fa8c71096dad0ca3daab5994c39a556e58fb0a5449acae7eb94",
"description": "Legal Discovery",
"category": "industry",
"tags": [
"industry",
"legal"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "lineage_child",
"file": "lineage_child.rvf",
"size": 26444,
"size_human": "25.8 KB",
"sha256": "557b9b8dc0de3f613150418991c0ea0a7329d3cbaece865a682219f440026605",
"description": "Lineage Child",
"category": "lineage",
"tags": [
"lineage",
"lineage"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "lineage_parent",
"file": "lineage_parent.rvf",
"size": 52444,
"size_human": "51.2 KB",
"sha256": "ce8469d66b09738e5726d45e2406f757c1f53ae9918bef4cf4afe80919667113",
"description": "Lineage Parent",
"category": "lineage",
"tags": [
"lineage",
"lineage"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "linux_microkernel",
"file": "linux_microkernel.rvf",
"size": 14370,
"size_human": "14.0 KB",
"sha256": "b4da7e039228608335e7e798923c8ea32ee63e5901939e0b42768d8c7bb4bd4f",
"description": "Linux Microkernel",
"category": "compute",
"tags": [
"compute",
"linux"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "mcp_in_rvf",
"file": "mcp_in_rvf.rvf",
"size": 32010,
"size_human": "31.3 KB",
"sha256": "8f9a873f5c6cee67d2854c264e431e074b4612e37719ccc32455c9b46a4ed0f0",
"description": "Mcp In Rvf",
"category": "integration",
"tags": [
"integration",
"mcp"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "medical_imaging",
"file": "medical_imaging.rvf",
"size": 308844,
"size_human": "301.6 KB",
"sha256": "ee727e29cb8596bdb63df6554d7bc43d5348bdde1dc2aea2b20a8c3456bd6215",
"description": "Medical Imaging",
"category": "industry",
"tags": [
"industry",
"medical"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "multimodal_fusion",
"file": "multimodal_fusion.rvf",
"size": 822844,
"size_human": "803.6 KB",
"sha256": "bf6a8111bfcd329a1d8507662d7062fd644207b9b2ef99ca45fbe3a477a6588c",
"description": "Multimodal Fusion",
"category": "core",
"tags": [
"core",
"multimodal"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "network_sync_a",
"file": "network_sync_a.rvf",
"size": 52444,
"size_human": "51.2 KB",
"sha256": "f5c4d94d80d62ff638af1321de65c0125703638ca4c6d206d06a6a0b61c541c4",
"description": "Network Sync A",
"category": "network",
"tags": [
"network",
"network"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "network_sync_b",
"file": "network_sync_b.rvf",
"size": 52444,
"size_human": "51.2 KB",
"sha256": "79e9d0b3b5dc1371b5eccfe505b6235e2e68e04cec793e1ab6df305c4b9cbd85",
"description": "Network Sync B",
"category": "network",
"tags": [
"network",
"network"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "network_telemetry",
"file": "network_telemetry.rvf",
"size": 16284,
"size_human": "15.9 KB",
"sha256": "7668934d4187955ab7f1ffded48c07352eeb8c7ff5a8ffd9c3d14d9158de243d",
"description": "Network Telemetry",
"category": "network",
"tags": [
"network",
"network"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "posix_fileops",
"file": "posix_fileops.rvf",
"size": 52444,
"size_human": "51.2 KB",
"sha256": "3375857824f0d64109cbe93d9fc41b0e1d3a1797bb192e8d7826088dfb14bd67",
"description": "Posix Fileops",
"category": "core",
"tags": [
"core",
"posix"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "postgres_bridge",
"file": "postgres_bridge.rvf",
"size": 154844,
"size_human": "151.2 KB",
"sha256": "e84b17c1ed161884c1a1b36e6a7dabe1a829f18faa61dca282cc699c73ad19a9",
"description": "Postgres Bridge",
"category": "integration",
"tags": [
"integration",
"postgres"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "progressive_index",
"file": "progressive_index.rvf",
"size": 2600444,
"size_human": "2.5 MB",
"sha256": "c359ec944a99f35745612680452f10390414b3e8956c1fbc5afa2f124156bc41",
"description": "Progressive Index",
"category": "core",
"tags": [
"core",
"progressive"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "quantization",
"file": "quantization.rvf",
"size": 1544444,
"size_human": "1.5 MB",
"sha256": "0c2457efa13276c4aa858535c2b15bd4501b5b6f3bbef78e896a5975495f3b60",
"description": "Quantization",
"category": "core",
"tags": [
"core",
"quantization"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "rag_pipeline",
"file": "rag_pipeline.rvf",
"size": 310044,
"size_human": "302.8 KB",
"sha256": "003f77aa8bf8a26be7c27f402111d1caf4a14f2862c2d5cb742e7c1d6c3f42b8",
"description": "Rag Pipeline",
"category": "core",
"tags": [
"core",
"rag"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "reasoning_child",
"file": "reasoning_child.rvf",
"size": 8244,
"size_human": "8.1 KB",
"sha256": "de1ababf338bc7cc34316268aed5c1278a5665a86603515deb5c9f30e78eb01d",
"description": "Reasoning Child",
"category": "lineage",
"tags": [
"lineage",
"reasoning"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "reasoning_grandchild",
"file": "reasoning_grandchild.rvf",
"size": 162,
"size_human": "162 B",
"sha256": "dd36c1b6946f3703e65606d880d26e50796baa8569ddcb4615a47dbf4fe74867",
"description": "Reasoning Grandchild",
"category": "lineage",
"tags": [
"lineage",
"reasoning"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "reasoning_parent",
"file": "reasoning_parent.rvf",
"size": 5644,
"size_human": "5.5 KB",
"sha256": "d514e7f5a684981239d2afe3f3df6445bee7d654d796992fe1acf9afb5b4ae6e",
"description": "Reasoning Parent",
"category": "lineage",
"tags": [
"lineage",
"reasoning"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "recommendation",
"file": "recommendation.rvf",
"size": 104444,
"size_human": "102.0 KB",
"sha256": "3de7076f4776071ad0e8068ad21ba764025fec8a41148f22e49690f3f57a3015",
"description": "Recommendation",
"category": "core",
"tags": [
"core",
"recommendation"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "ruvbot",
"file": "ruvbot.rvf",
"size": 52044,
"size_human": "50.8 KB",
"sha256": "f054fedd3e3fa77606bed9465d06d3013803e9723c048b99fda5aa7f42468b34",
"description": "Ruvbot",
"category": "ai",
"tags": [
"ai",
"ruvbot"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "ruvllm_inference",
"file": "ruvllm_inference.rvf",
"size": 135612,
"size_human": "132.4 KB",
"sha256": "dce0d960f6f945d8f20c81dca9805d572887801a454085153502d9b6f8dec31d",
"description": "Ruvllm Inference",
"category": "ai",
"tags": [
"ai",
"ruvllm"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "sealed_engine",
"file": "sealed_engine.rvf",
"size": 212474,
"size_human": "207.5 KB",
"sha256": "0480b43e0f4377248fdb68469567e9dff39787ad902e4bb7680521d26ae8057b",
"description": "Sealed Engine",
"category": "security",
"tags": [
"security",
"sealed"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "self_booting",
"file": "self_booting.rvf",
"size": 30994,
"size_human": "30.3 KB",
"sha256": "308a4f2f33c30102de745152232d4f46657fc1db7afb65d64176108576773eaf",
"description": "Self Booting",
"category": "compute",
"tags": [
"compute",
"self"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "semantic_search",
"file": "semantic_search.rvf",
"size": 772444,
"size_human": "754.3 KB",
"sha256": "4fe45c0bc9dc3035ae51edea34c0f58a1953254b3d994804a5709700042c9cdd",
"description": "Semantic Search",
"category": "core",
"tags": [
"core",
"semantic"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "serverless",
"file": "serverless.rvf",
"size": 520444,
"size_human": "508.2 KB",
"sha256": "3a0813c6e82f363e81a55caf7466e1aeb1b4ad12d4816e76c651cfdbeccb7b8d",
"description": "Serverless",
"category": "integration",
"tags": [
"integration",
"serverless"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "swarm_knowledge",
"file": "swarm_knowledge.rvf",
"size": 87728,
"size_human": "85.7 KB",
"sha256": "f33fb4ec18b8aebb8ab268bd78fb47c743b1cc4970c5b92a067cf0b818095d7b",
"description": "Swarm Knowledge",
"category": "ai",
"tags": [
"ai",
"swarm"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "tee_attestation",
"file": "tee_attestation.rvf",
"size": 103644,
"size_human": "101.2 KB",
"sha256": "2b6ebb3bfecbddd7ca31ec7a64b1813fa48194d6f1011b6ad4e6d6063ced9c0e",
"description": "Tee Attestation",
"category": "security",
"tags": [
"security",
"tee"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "tool_cache",
"file": "tool_cache.rvf",
"size": 26444,
"size_human": "25.8 KB",
"sha256": "62fb788095d44086617367a859787135da343c548794f11914c407abdc379b42",
"description": "Tool Cache",
"category": "ai",
"tags": [
"ai",
"tool"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
},
{
"name": "zero_knowledge",
"file": "zero_knowledge.rvf",
"size": 52444,
"size_human": "51.2 KB",
"sha256": "a062bbebfb8180700be47a35410ba9c9004a2e7674af8800eb9ac619b6850535",
"description": "Zero Knowledge",
"category": "security",
"tags": [
"security",
"zero"
],
"segments": [
"VEC",
"META"
],
"created": "2026-02-20"
}
],
"categories": {
"core": "Basic vector storage, search, and indexing",
"ai": "AI agent, embedding, RAG, and chatbot examples",
"security": "Attestation, ZK proofs, access control, sealed engines",
"compute": "eBPF, WASM, self-booting, IoT, kernels",
"lineage": "COW chains, derivation trees, reasoning chains",
"industry": "Finance, medical, legal domain examples",
"network": "Sync, handoff, telemetry, distributed examples",
"integration": "MCP, PostgreSQL, serverless, Claude Code bridges"
}
}

View file

@ -0,0 +1,173 @@
# @ruvector/pi-brain
CLI and SDK for **π.ruv.io** — the RuVector shared AI brain. Search, share, and transfer knowledge across AI sessions with cryptographic verification and federated learning.
## Install
```bash
npm install @ruvector/pi-brain
```
Or run directly:
```bash
npx @ruvector/pi-brain search "graph neural network"
```
## Authentication
Set your API key via environment variable:
```bash
export PI=$(echo -n "my-secret" | sha256sum | cut -c1-32)
```
Your identity is **pseudonymous** — the server derives a contributor ID from your key via SHAKE-256. No PII is stored.
## CLI
```bash
# Search the collective brain
pi-brain search "Byzantine consensus"
# Share knowledge
pi-brain share --category pattern \
--title "My Discovery" \
--content "Detailed explanation..."
# Browse & manage
pi-brain list --category architecture --limit 10
pi-brain status
pi-brain health
# Vote on quality
pi-brain vote <memory-id> up
# Start MCP server (stdio transport for Claude Code)
pi-brain mcp
# Start MCP server (SSE transport)
pi-brain mcp --transport sse
```
### Commands
| Command | Description |
|---------|-------------|
| `search <query>` | Semantic search across shared knowledge |
| `share` | Contribute a memory to the brain |
| `list` | List memories with optional filters |
| `get <id>` | Get a specific memory |
| `vote <id> <up\|down>` | Vote on memory quality |
| `delete <id>` | Delete a memory you own |
| `status` | Brain stats (memories, graph, embeddings) |
| `health` | Service health check |
| `drift` | Embedding drift report |
| `partition` | Knowledge graph topology |
| `transfer` | Domain expansion transfer learning |
| `mcp` | Start MCP server for Claude Code |
## SDK
```typescript
import { PiBrainClient } from '@ruvector/pi-brain';
const brain = new PiBrainClient({
apiKey: process.env.PI,
url: 'https://pi.ruv.io', // default
});
// Search
const results = await brain.search({
query: 'attention mechanism',
category: 'architecture',
limit: 5,
});
// Share knowledge
const { id } = await brain.share({
category: 'pattern',
title: 'Federated Averaging',
content: 'Description of the FedAvg algorithm...',
tags: ['federated', 'learning'],
});
// Vote
await brain.vote(id, 'up');
// Status
const status = await brain.status();
console.log(status.total_memories); // 213+
```
### API
#### `new PiBrainClient(options?)`
| Option | Default | Description |
|--------|---------|-------------|
| `url` | `https://pi.ruv.io` | Brain server URL |
| `apiKey` | `$PI` or `$BRAIN_API_KEY` | Authentication key |
#### Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `search(opts)` | `Memory[]` | Hybrid keyword + embedding search |
| `share(opts)` | `{ id, quality_score }` | Contribute knowledge |
| `list(category?, limit?)` | `Memory[]` | List memories |
| `get(id)` | `Memory` | Get by ID |
| `vote(id, direction)` | `void` | Up/down vote |
| `delete(id)` | `void` | Delete owned memory |
| `status()` | `Status` | Brain statistics |
| `health()` | `Health` | Service health |
| `drift(domain?)` | `DriftReport` | Embedding drift |
| `partition(domain?)` | `Partition` | Graph topology |
| `transfer(source, target)` | `TransferResult` | Domain transfer |
## MCP Integration
Register with Claude Code:
```bash
# Quick setup
claude mcp add pi-brain -- npx @ruvector/pi-brain mcp
# Or via the ruvector CLI
npx ruvector brain mcp-register
```
Available MCP tools: `brain_search`, `brain_share`, `brain_vote`, `brain_list`, `brain_get`, `brain_status`, `brain_drift`, `brain_partition`, `brain_transfer`, `brain_delete`.
## Categories
| Category | Description |
|----------|-------------|
| `architecture` | System design, topology, data flow |
| `pattern` | Reusable solutions, algorithms |
| `security` | Auth, validation, cryptography |
| `solution` | Implementation approaches |
| `convention` | Standards, naming, organization |
| `performance` | Optimization, benchmarks |
| `tooling` | Libraries, frameworks, CLIs |
## How It Works
The π brain uses **hybrid search** combining:
- **Keyword matching** (85%) — Word-boundary matching with title/tag/content weighting
- **Neural embeddings** (10%) — 128-dim ruvllm vectors via cosine similarity
- **Reputation** (5%) — Contributor quality scores from vote history
Knowledge is verified through **SHAKE-256 witness chains** and protected by **Byzantine-tolerant federated learning** with 2σ outlier filtering.
## Links
- **Homepage**: [pi.ruv.io](https://pi.ruv.io)
- **API Manifest**: [brain-manifest.json](https://pi.ruv.io/.well-known/brain-manifest.json)
- **Agent Guide**: [agent-guide.md](https://pi.ruv.io/.well-known/agent-guide.md)
- **GitHub**: [ruvnet/ruvector](https://github.com/ruvnet/ruvector)
- **Parent CLI**: [ruvector](https://www.npmjs.com/package/ruvector)
## License
MIT

View file

@ -0,0 +1,40 @@
{
"name": "@ruvector/pi-brain",
"version": "0.1.0",
"description": "CLI and SDK for π — the RuVector shared brain. Share, search, and transfer learning across AI sessions.",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"pi-brain": "dist/cli.js",
"π": "dist/cli.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"start": "node dist/cli.js",
"dev": "tsc --watch"
},
"keywords": ["pi", "brain", "ruvector", "mcp", "shared-learning", "claude", "ai"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector",
"directory": "npm/packages/pi-brain"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
}
}

Some files were not shown because too many files have changed in this diff Show more