mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-08-22 07:03:42 +00:00
Merge remote-tracking branch 'origin/main' into agent/fix-pr-723
This commit is contained in:
commit
6dfc47a17e
26 changed files with 3767 additions and 4848 deletions
34
.github/workflows/ruvector-npm-ci.yml
vendored
34
.github/workflows/ruvector-npm-ci.yml
vendored
|
|
@ -62,8 +62,11 @@ jobs:
|
|||
set +e
|
||||
./node_modules/.bin/tsc 2>&1 | tail -5
|
||||
TSC_EXIT=$?
|
||||
mkdir -p dist/core/onnx/pkg
|
||||
cp -r src/core/onnx/pkg/. dist/core/onnx/pkg/ 2>/dev/null || true
|
||||
# TypeScript does not emit the JavaScript/MJS/WASM assets consumed by
|
||||
# the ONNX contract tests and published package. Mirror the package
|
||||
# build script by copying the complete runtime directory.
|
||||
mkdir -p dist/core/onnx
|
||||
cp -r src/core/onnx/. dist/core/onnx/
|
||||
if [ ! -f dist/index.js ]; then
|
||||
echo "::error::dist/index.js missing — build truly failed (not just pre-existing TS errors)"
|
||||
exit 1
|
||||
|
|
@ -95,7 +98,10 @@ jobs:
|
|||
node-version: '20'
|
||||
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
|
||||
|
||||
- name: Verify native backend is installed
|
||||
run: node -e "require('ruvector-core-linux-x64-gnu')"
|
||||
|
||||
- name: Download dist
|
||||
uses: actions/download-artifact@v4
|
||||
|
|
@ -140,7 +146,10 @@ jobs:
|
|||
node-version: '20'
|
||||
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
|
||||
|
||||
- name: Verify native backend is installed
|
||||
run: node -e "require('ruvector-core-linux-x64-gnu')"
|
||||
|
||||
- name: Download dist
|
||||
uses: actions/download-artifact@v4
|
||||
|
|
@ -237,7 +246,10 @@ jobs:
|
|||
node-version: '20'
|
||||
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
|
||||
|
||||
- name: Verify native backend is installed
|
||||
run: node -e "require('ruvector-core-linux-x64-gnu')"
|
||||
|
||||
- name: Download dist
|
||||
uses: actions/download-artifact@v4
|
||||
|
|
@ -352,7 +364,10 @@ jobs:
|
|||
node-version: '20'
|
||||
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
|
||||
|
||||
- name: Verify native backend is installed
|
||||
run: node -e "require('ruvector-core-linux-x64-gnu')"
|
||||
|
||||
- name: Download dist
|
||||
uses: actions/download-artifact@v4
|
||||
|
|
@ -460,7 +475,10 @@ jobs:
|
|||
node-version: '20'
|
||||
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
|
||||
|
||||
- name: Verify native backend is installed
|
||||
run: node -e "require('ruvector-core-linux-x64-gnu')"
|
||||
|
||||
- name: Download dist
|
||||
uses: actions/download-artifact@v4
|
||||
|
|
@ -659,6 +677,8 @@ jobs:
|
|||
- tarball-integrity
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check all jobs passed
|
||||
run: |
|
||||
results='${{ toJSON(needs) }}'
|
||||
|
|
|
|||
6
.github/workflows/supply-chain.yml
vendored
6
.github/workflows/supply-chain.yml
vendored
|
|
@ -58,10 +58,14 @@ jobs:
|
|||
name: dependency-review (PRs only)
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: dependency-review-action
|
||||
# The repository dependency graph is not currently exposed by GitHub,
|
||||
# so the action returns "Dependency review is not supported". Keep the
|
||||
# other four supply-chain layers hard-gated while #689 tracks enabling
|
||||
# this service-side prerequisite.
|
||||
continue-on-error: true
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
|
|
|
|||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -5,6 +5,36 @@ All notable changes to RuVector will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [ruvector npm 0.2.37] - 2026-07-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restore the release CI's native backend installation so functional,
|
||||
learning, performance, and recall gates exercise HNSW instead of the
|
||||
in-memory fallback.
|
||||
- Preserve the Rust core's default HNSW behavior when JavaScript callers omit
|
||||
`hnswConfig`; the wrapper previously selected the exact FlatIndex path.
|
||||
- Normalize the package binary metadata and its integration assertion.
|
||||
- Remove the npm workspace's critical `tar` advisory with a patched override.
|
||||
- Correct the Shor factoring milestone table to distinguish a failed
|
||||
35-factor experiment from the separate Schnorr/QAOA hybrid result.
|
||||
|
||||
## [ruvector npm 0.2.36] - 2026-07-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Start the stdio MCP transport when launched through `ruvector mcp start`
|
||||
([#715](https://github.com/ruvnet/ruvector/issues/715)).
|
||||
- Keep ONNX model-loader diagnostics on stderr so they cannot corrupt MCP
|
||||
JSON-RPC frames ([#710](https://github.com/ruvnet/ruvector/issues/710)).
|
||||
|
||||
### Security
|
||||
|
||||
- Update `anyhow` to 1.0.104 and `memmap2` to 0.9.11, removing the
|
||||
corresponding RustSec soundness exceptions.
|
||||
- Update the MCP SDK to 1.29.x, clearing its shared-transport data-leak and
|
||||
ReDoS advisories, and remove the unused `js-beautify` dependency.
|
||||
|
||||
## [hailo-backend] - 2026-05-03
|
||||
|
||||
Branch-only entry; not yet released to a versioned tag. 38 iters /
|
||||
|
|
|
|||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -235,9 +235,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
|
|
@ -5371,9 +5371,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.10"
|
||||
version = "0.9.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
|
||||
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"stable_deref_trait",
|
||||
|
|
|
|||
|
|
@ -87,14 +87,20 @@ fn cmd_build(args: &[String]) {
|
|||
.insert(id.to_string(), random_unit_vector(&mut rng, dim))
|
||||
.expect("insert failed");
|
||||
if id % 50_000 == 0 {
|
||||
eprintln!("inserted {id}/{n} ({:.1}s elapsed)", t0.elapsed().as_secs_f64());
|
||||
eprintln!(
|
||||
"inserted {id}/{n} ({:.1}s elapsed)",
|
||||
t0.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
eprintln!("insert phase done in {:.1}s", t0.elapsed().as_secs_f64());
|
||||
|
||||
let t1 = Instant::now();
|
||||
index.build().expect("build failed"); // build() also calls save() since storage_path is set
|
||||
eprintln!("build+save phase done in {:.1}s", t1.elapsed().as_secs_f64());
|
||||
eprintln!(
|
||||
"build+save phase done in {:.1}s",
|
||||
t1.elapsed().as_secs_f64()
|
||||
);
|
||||
|
||||
println!("BUILD_OK n={n} dim={dim} dir={}", dir.display());
|
||||
}
|
||||
|
|
@ -103,7 +109,9 @@ fn cmd_measure(args: &[String]) {
|
|||
let dir = PathBuf::from(get_flag(args, "--dir", "/tmp/mmap674"));
|
||||
let mode = get_flag(args, "--mode", "owned");
|
||||
let dim: usize = get_flag(args, "--dim", "128").parse().expect("--dim");
|
||||
let queries: usize = get_flag(args, "--queries", "1000").parse().expect("--queries");
|
||||
let queries: usize = get_flag(args, "--queries", "1000")
|
||||
.parse()
|
||||
.expect("--queries");
|
||||
let warmup: usize = get_flag(args, "--warmup", "50").parse().expect("--warmup");
|
||||
let k: usize = get_flag(args, "--k", "10").parse().expect("--k");
|
||||
|
||||
|
|
|
|||
13
deny.toml
13
deny.toml
|
|
@ -123,19 +123,6 @@ ignore = [
|
|||
# code, never reachable via untrusted input. Re-review 2026-08-01.
|
||||
"RUSTSEC-2026-0192",
|
||||
|
||||
# memmap2 0.9.x — unsound `Unchecked pointer offset` API (RUSTSEC-2026-0186,
|
||||
# "unsound", not an exploitable vuln). Transitive via safetensors/candle
|
||||
# mmap loading and other crates; no fixed release on the 0.9 line yet.
|
||||
# We don't pass attacker-controlled offsets to memmap2. Re-review 2026-08-01.
|
||||
"RUSTSEC-2026-0186",
|
||||
|
||||
# anyhow 1.0.102 — unsoundness in `Error::downcast_mut()` (RUSTSEC-2026-0190,
|
||||
# "unsound", not an exploitable vuln). Triggering it requires downcasting an
|
||||
# `anyhow::Error` to a mismatched mutable type — a pattern we never use; our
|
||||
# usage is construct/propagate/`Display`/read-only `downcast_ref`. Single
|
||||
# version (1.0.102) in the tree. Bump to the patched anyhow once it ships and
|
||||
# remove this entry. Re-review 2026-08-01.
|
||||
"RUSTSEC-2026-0190",
|
||||
]
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
| Shor's original paper | 1994 | Theoretical | 0 |
|
||||
| First experimental demo | 2001 | 15 = 3 × 5 | 7 (NMR) |
|
||||
| Photonic factoring | 2012 | 21 = 3 × 7 | 10 |
|
||||
| IBM superconducting | 2019 | 35 = 5 × 7 | 16 |
|
||||
| Variational hybrid | 2023 | 261,980,999 (claim disputed) | 10 |
|
||||
| IBM superconducting | 2019 | Attempted 35 (experiment did not factor it) | 16 |
|
||||
| Schnorr/QAOA hybrid (not Shor) | 2023 | 261,980,999,226,229 | 10 |
|
||||
| Current NISQ frontier | 2026 | ~1,000-10,000 range (noisy) | 50-100 |
|
||||
| ruQu simulator | 2026 | ~32,767 (15-bit, clean sim) | 25 |
|
||||
|
||||
|
|
|
|||
163
docs/research/ruvector-sota-security-review-2026-07.md
Normal file
163
docs/research/ruvector-sota-security-review-2026-07.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# RuVector SOTA, Security, and Package Review — 2026-07-26
|
||||
|
||||
## Scope and method
|
||||
|
||||
This review covers the complete tracked release surface at `main`
|
||||
(`6a6c39e66`): 197 Rust workspace packages, 169 tracked npm manifests, 47
|
||||
GitHub Actions workflows, and approximately 382,000 lines of Rust,
|
||||
TypeScript, JavaScript, and native source under `crates/`, `npm/packages/`,
|
||||
and `packages/`.
|
||||
|
||||
The review combined:
|
||||
|
||||
- manifest and release-workflow inventory;
|
||||
- RustSec and `cargo-deny` dependency-graph checks;
|
||||
- npm production-dependency audit;
|
||||
- source-wide searches for unsafe Rust, process execution, unchecked panics,
|
||||
path handling, protocol stdout, and release bypasses;
|
||||
- targeted execution of the `ruvector` npm build, package verification, MCP
|
||||
initialize handshake, signal cleanup, and policy tests;
|
||||
- comparison with 2025–2026 work on filtered ANN, dynamic quantization,
|
||||
compressed graph/ID storage, hybrid sparse+dense retrieval, and fresh
|
||||
disk-backed ANN.
|
||||
|
||||
Raw pattern counts are triage signals, not vulnerability counts. Generated
|
||||
bindings, tests, examples, and deliberately low-level SIMD/FFI code account
|
||||
for much of the `unsafe`, `unwrap`, and process-execution footprint.
|
||||
|
||||
## Executive assessment
|
||||
|
||||
RuVector's algorithm portfolio is unusually broad and substantially aligned
|
||||
with the current research frontier. The repository already contains HNSW,
|
||||
DiskANN, SPANN, ACORN/filter-aware search, RaBitQ and other quantization,
|
||||
hybrid BM25+dense fusion, sparse and multivector representations, GNN
|
||||
reranking, Matryoshka evaluation, RVF persistence, Postgres integration,
|
||||
WASM/N-API bindings, and dedicated SOTA benchmark runners.
|
||||
|
||||
The largest gap is not another ANN algorithm. It is productization across the
|
||||
many implementations: durable metadata and filter semantics across every
|
||||
binding, common recall/latency/memory benchmarks, streaming-update evaluation,
|
||||
safe-by-default agent tool execution, and a release process that can prove
|
||||
exactly what was built and published.
|
||||
|
||||
## Current SOTA alignment
|
||||
|
||||
| Area | RuVector coverage | Assessment |
|
||||
|---|---|---|
|
||||
| In-memory ANN | HNSW, learned and coherence variants, SIMD kernels | Strong |
|
||||
| Disk / billion-scale ANN | DiskANN, SPANN, delta/LSM and repair research | Strong portfolio; needs one shared freshness benchmark |
|
||||
| Filtered ANN | Filtered search and ACORN-family work | Algorithmically current; metadata durability and selectivity-aware planning remain the limiting integration gaps |
|
||||
| Compression | PQ/scalar/binary paths, RaBitQ, Matryoshka runners | Strong; add graph-edge/vector-ID compression and streaming retraining measurements |
|
||||
| Hybrid retrieval | BM25, sparse vectors, RRF/RSF/score fusion | Strong in Rust; Node's top-level `ruvector` API remains incomplete |
|
||||
| Late interaction | Multivector support and reranking components | Present but not yet a single documented, benchmarked MaxSim product path |
|
||||
| Freshness | Delta indexes, repair, LSM research, snapshot/raft components | Broad building blocks; no unified insert/delete/update SLO gate |
|
||||
| Portability | Native Rust, N-API, WASM, Postgres, RVF | Excellent breadth; API parity is inconsistent |
|
||||
| Evaluation | `ruvector-sota-bench`, VDBBench, MTEB and focused benchmarks | Good foundation; results need reproducible hardware/dataset manifests and regression budgets |
|
||||
|
||||
### Highest-value SOTA work
|
||||
|
||||
1. **Filtered ANN as a query-planning problem.** Choose exact scan, IVF,
|
||||
graph traversal, or pre/inline/post-filtering from measured selectivity and
|
||||
vector/filter correlation. Add difficult-filter datasets and recall
|
||||
stability gates, not only unfiltered ANN recall.
|
||||
2. **Fresh quantization under updates.** Measure quality drift after inserts,
|
||||
deletes, and distribution shifts; trigger local codebook repair before a
|
||||
global rebuild.
|
||||
3. **Compress graph structure, not only vectors.** Vector IDs and HNSW/IVF
|
||||
adjacency can dominate memory after aggressive vector quantization.
|
||||
4. **One multistage retrieval contract.** Standardize dense+sparse candidate
|
||||
generation, RRF/RSF fusion, optional MaxSim/GNN reranking, and provenance
|
||||
of every score.
|
||||
5. **Budget-aware serving.** Expose recall target, latency/evaluation budget,
|
||||
and freshness target as the stable API; keep index-specific tuning internal.
|
||||
|
||||
## Security findings
|
||||
|
||||
### Fixed in this change
|
||||
|
||||
- **Critical availability — MCP launcher no-op (#715).** The CLI required
|
||||
`mcp-server.js`, but that module only started when executed directly. The
|
||||
module now exports `main()` and the CLI invokes it explicitly.
|
||||
- **Protocol integrity — stdout corruption (#710).** All ONNX loader status
|
||||
messages now use stderr; stdout remains JSON-RPC-only.
|
||||
- **MCP command injection.** `workers_create` previously interpolated raw MCP
|
||||
fields into `execSync`. It now uses `execFileSync` with an argument vector.
|
||||
- **Rust soundness advisories.** `anyhow` is updated to 1.0.104 and `memmap2`
|
||||
to 0.9.11; their RustSec exceptions are removed.
|
||||
- **npm dependency exposure.** The MCP SDK is updated to 1.29.x and the unused
|
||||
`js-beautify` dependency (and its vulnerable glob stack) is removed.
|
||||
|
||||
### Open risks requiring separate changes
|
||||
|
||||
1. **MCP defaults remain permissive.** With no environment policy, all tools
|
||||
are exposed, including tools that mutate files or launch subprocesses.
|
||||
Make `readonly` the default in the next semver-major release and require an
|
||||
explicit profile for process-launching tools.
|
||||
2. **Process execution is widespread.** Replace remaining `execSync` command
|
||||
strings with `execFile`/`spawn` argument arrays, then enforce this with a
|
||||
lint rule on MCP, CLI, deploy, and installer code.
|
||||
3. **Release gates can report false success.** Several workflows append
|
||||
`|| true` or `|| echo` to `npm publish`; several Cargo paths use
|
||||
`--allow-dirty`. Publishing must fail closed and verify the registry
|
||||
version, tarball digest, SBOM, provenance, and tag after upload.
|
||||
4. **Workspace npm install is not reproducible on Linux.** `npm ci` currently
|
||||
attempts to install a Darwin/ARM64 workspace package and exits with
|
||||
`EBADPLATFORM`. Platform binaries need to be optional dependencies or
|
||||
excluded from the root workspace install.
|
||||
5. **The aggregate npm lock contains known advisories.** The workspace audit
|
||||
reported 46 affected dependency nodes before package scoping. Triage by
|
||||
reachable production package and update direct owners; do not hide the
|
||||
aggregate result behind blanket audit exceptions.
|
||||
6. **Post-quantum dependency maintenance.** RustSec now marks the PQClean-based
|
||||
`pqcrypto-*` ecosystem unmaintained. Select and benchmark a maintained
|
||||
ML-KEM/ML-DSA implementation before the next cryptography release.
|
||||
7. **Residual dependency warnings.** `spin 0.9.8` is yanked but remains
|
||||
transitive through `lazy_static`; the MCP SDK also carries a moderate Hono
|
||||
Windows static-file advisory. RuVector's MCP package imports only the stdio
|
||||
server path, so the Hono static-file handler is unreachable here, but the
|
||||
dependency should still be upgraded as soon as the SDK accepts Hono 2.x.
|
||||
8. **Unsafe/panic budgets are not centralized.** Add crate-level policy:
|
||||
`unsafe` only in named FFI/SIMD modules with safety invariants, and no
|
||||
`unwrap`/`expect` on production input paths.
|
||||
|
||||
## Package and API findings
|
||||
|
||||
- Issues #704–#707 are already materially addressed on `main`: RVF rejects
|
||||
unsupported metadata instead of dropping it, WASM byte export/open exists,
|
||||
SONA N-API buffers preserve residual identity, and Node hybrid-search docs
|
||||
disclose the unshipped API.
|
||||
- RVF metadata still needs an end-to-end schema, durable segment storage,
|
||||
reload, result retrieval, and identical filter typing across Rust, N-API,
|
||||
WASM, and Postgres.
|
||||
- `HybridSearch` remains Rust-only in the top-level Node package. Bind the
|
||||
production Rust implementation rather than introducing another JavaScript
|
||||
scoring implementation.
|
||||
- The repository has many publishable manifests but release automation covers
|
||||
only a subset. Generate a canonical package graph with owner, source,
|
||||
registry, version, native platforms, test command, and publish workflow.
|
||||
|
||||
## Required release gates
|
||||
|
||||
Every published package should pass:
|
||||
|
||||
1. clean checkout and locked dependency install;
|
||||
2. build, unit tests, integration tests, and package/tarball smoke test;
|
||||
3. RustSec/npm audit with documented reachability for any exception;
|
||||
4. secret scan, SBOM, license/source policy, and artifact checksums;
|
||||
5. native/WASM API parity checks where applicable;
|
||||
6. registry publish with provenance, followed by registry version and digest
|
||||
verification;
|
||||
7. rollback/deprecation instructions recorded before release.
|
||||
|
||||
For `ruvector@0.2.36`, the relevant focused gates are the end-to-end MCP
|
||||
initialize test, stdout JSON purity, clean signal shutdown, full npm package
|
||||
tests, TypeScript build, distribution verification, and `npm pack` smoke test.
|
||||
|
||||
## Primary references
|
||||
|
||||
- [DiskANN: fast, fresh, and filtered vector search](https://github.com/microsoft/DiskANN)
|
||||
- [Survey of Filtered Approximate Nearest Neighbor Search (2025)](https://arxiv.org/abs/2505.06501)
|
||||
- [Filtered ANN system design and performance analysis (2026)](https://arxiv.org/abs/2602.11443)
|
||||
- [Quantization for vector search under streaming updates](https://arxiv.org/abs/2512.18335)
|
||||
- [Lossless compression of vector IDs for ANN](https://arxiv.org/abs/2501.10479)
|
||||
- [Qdrant data, filter, index, and quantization documentation](https://qdrant.tech/documentation/manage-data/)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8141
npm/package-lock.json
generated
8141
npm/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -38,6 +38,7 @@
|
|||
"fast-xml-parser": ">=5.7.0",
|
||||
"protobufjs": ">=7.5.6",
|
||||
"@google-cloud/redis": ">=5.0.0",
|
||||
"vm2": ">=3.11.5"
|
||||
"vm2": ">=3.11.5",
|
||||
"tar": "7.5.22"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"@google-cloud/monitoring": "^4.0.0",
|
||||
"@google-cloud/compute": "^4.0.0",
|
||||
"@google-cloud/cloud-sql-connector": "^1.3.0",
|
||||
"@google-cloud/redis": "^3.0.0",
|
||||
"@google-cloud/redis": "^5.3.0",
|
||||
"@google-cloud/logging": "^11.0.0",
|
||||
"node-cron": "^3.0.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8138,13 +8138,16 @@ const mcpCmd = program.command('mcp').description('MCP (Model Context Protocol)
|
|||
mcpCmd.command('start')
|
||||
.description('Start the RuVector MCP server')
|
||||
.action(() => {
|
||||
// Execute the mcp-server.js directly
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.js');
|
||||
if (!fs.existsSync(mcpServerPath)) {
|
||||
console.error(chalk.red('Error: MCP server not found at'), mcpServerPath);
|
||||
process.exit(1);
|
||||
}
|
||||
require(mcpServerPath);
|
||||
const { main } = require(mcpServerPath);
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
});
|
||||
|
||||
mcpCmd.command('info')
|
||||
|
|
@ -10277,4 +10280,3 @@ if (require.main === module) {
|
|||
module.exports = { atomicWriteFileSync, readIntelStoreSafe, Intelligence };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3183,13 +3183,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||
}
|
||||
|
||||
case 'workers_create': {
|
||||
const name = args.name;
|
||||
const preset = args.preset || 'quick-scan';
|
||||
const triggers = args.triggers;
|
||||
const name = String(args.name || '');
|
||||
const preset = String(args.preset || 'quick-scan');
|
||||
const triggers = args.triggers == null ? null : String(args.triggers);
|
||||
try {
|
||||
let cmd = `npx agentic-flow@alpha workers create "${name}" --preset ${preset}`;
|
||||
if (triggers) cmd += ` --triggers "${triggers}"`;
|
||||
const result = execSync(cmd, {
|
||||
// Never interpolate MCP-controlled fields into a shell command.
|
||||
// execFileSync passes each value as one argument, so names/triggers
|
||||
// containing quotes or shell metacharacters cannot escape to a shell.
|
||||
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const commandArgs = ['agentic-flow@alpha', 'workers', 'create', name, '--preset', preset];
|
||||
if (triggers) commandArgs.push('--triggers', triggers);
|
||||
const result = execFileSync(command, commandArgs, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
|
|
@ -4084,8 +4088,11 @@ async function main() {
|
|||
process.stdin.on('end', () => process.exit(0));
|
||||
}
|
||||
|
||||
module.exports = { main, loadBrainClient, BRAIN_MISSING_DEP_RESULT };
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
} else {
|
||||
module.exports = { loadBrainClient, BRAIN_MISSING_DEP_RESULT };
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"name": "ruvector",
|
||||
"version": "0.2.35",
|
||||
"version": "0.2.37",
|
||||
"description": "Self-learning vector database for Node.js \u2014 hybrid search, Graph RAG, FlashAttention-3, HNSW, 50+ attention mechanisms",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"ruvector": "./bin/cli.js"
|
||||
"ruvector": "bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').cpSync('src/core/onnx','dist/core/onnx',{recursive:true})\"",
|
||||
"verify-dist": "node scripts/verify-dist.js",
|
||||
"prepack": "npm run build && npm run verify-dist",
|
||||
"prepublishOnly": "npm run build && npm run verify-dist",
|
||||
"test": "node test/integration.js && node test/cli-commands.js && node test/db-workflow.js && node test/sigterm-cleanup.js && node test/mcp-policy.js && node test/startup-budget.js"
|
||||
"test": "node test/integration.js && node test/cli-commands.js && node test/db-workflow.js && node test/mcp-stdio.js && node test/sigterm-cleanup.js && node test/mcp-policy.js && node test/startup-budget.js"
|
||||
},
|
||||
"keywords": [
|
||||
"vector",
|
||||
|
|
@ -67,18 +67,17 @@
|
|||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ruvnet/ruvector.git",
|
||||
"url": "git+https://github.com/ruvnet/ruvector.git",
|
||||
"directory": "npm/packages/ruvector"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@ruvector/attention": "^0.1.3",
|
||||
"@ruvector/core": "^0.1.25",
|
||||
"@ruvector/gnn": "^0.1.22",
|
||||
"@ruvector/sona": "^0.1.4",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.1.0",
|
||||
"js-beautify": "^1.15.4",
|
||||
"ora": "^5.4.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export class ModelLoader {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
|
||||
console.error(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
|
||||
|
||||
const [modelBytes, tokenizerJson] = await Promise.all([
|
||||
this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
|
||||
|
|
@ -196,7 +196,7 @@ export class ModelLoader {
|
|||
const modelBytes = new Uint8Array(fs.readFileSync(modelPath));
|
||||
const tokenizerJson = fs.readFileSync(tokPath, 'utf8');
|
||||
if (modelBytes.length === 0 || tokenizerJson.length === 0) return null;
|
||||
console.log(` Disk cache hit: ${modelName}`);
|
||||
console.error(` Disk cache hit: ${modelName}`);
|
||||
return { modelBytes, tokenizerJson };
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -274,7 +274,7 @@ export class ModelLoader {
|
|||
const cache = await caches.open(this.cacheStorage);
|
||||
const cached = await cache.match(cacheKey);
|
||||
if (cached) {
|
||||
console.log(` Cache hit: ${cacheKey}`);
|
||||
console.error(` Cache hit: ${cacheKey}`);
|
||||
return responseType === 'arraybuffer'
|
||||
? await cached.arrayBuffer()
|
||||
: await cached.text();
|
||||
|
|
@ -285,7 +285,7 @@ export class ModelLoader {
|
|||
}
|
||||
|
||||
// Fetch from network
|
||||
console.log(` Downloading: ${url}`);
|
||||
console.error(` Downloading: ${url}`);
|
||||
const response = await this.fetchWithProgress(url);
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -365,7 +365,7 @@ export class ModelLoader {
|
|||
async clearCache() {
|
||||
if (typeof caches !== 'undefined') {
|
||||
await caches.delete(this.cacheStorage);
|
||||
console.log('Model cache cleared');
|
||||
console.error('Model cache cleared');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export class ModelLoader {
|
|||
throw new Error(`Unknown model: ${modelName}. Available: ${Object.keys(MODELS).join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
|
||||
console.error(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
|
||||
|
||||
const [modelBytes, tokenizerJson] = await Promise.all([
|
||||
this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
|
||||
|
|
@ -161,7 +161,7 @@ export class ModelLoader {
|
|||
const cache = await caches.open(this.cacheStorage);
|
||||
const cached = await cache.match(cacheKey);
|
||||
if (cached) {
|
||||
console.log(` Cache hit: ${cacheKey}`);
|
||||
console.error(` Cache hit: ${cacheKey}`);
|
||||
return responseType === 'arraybuffer'
|
||||
? await cached.arrayBuffer()
|
||||
: await cached.text();
|
||||
|
|
@ -172,7 +172,7 @@ export class ModelLoader {
|
|||
}
|
||||
|
||||
// Fetch from network
|
||||
console.log(` Downloading: ${url}`);
|
||||
console.error(` Downloading: ${url}`);
|
||||
const response = await this.fetchWithProgress(url);
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -252,7 +252,7 @@ export class ModelLoader {
|
|||
async clearCache() {
|
||||
if (typeof caches !== 'undefined') {
|
||||
await caches.delete(this.cacheStorage);
|
||||
console.log('Model cache cleared');
|
||||
console.error('Model cache cleared');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,19 @@ class VectorDBWrapper {
|
|||
const nativeOptions: any = {
|
||||
dimensions: options.dimensions,
|
||||
storagePath: options.storagePath,
|
||||
hnswConfig: options.hnswConfig,
|
||||
// The N-API binding maps an omitted hnswConfig to `None`, which selects
|
||||
// FlatIndex and unintentionally overrides ruvector-core's HNSW default.
|
||||
// Pass the documented defaults explicitly so the high-level VectorDB
|
||||
// remains an ANN database unless callers deliberately provide another
|
||||
// HNSW configuration.
|
||||
hnswConfig: options.hnswConfig === undefined
|
||||
? {
|
||||
m: 32,
|
||||
efConstruction: 200,
|
||||
efSearch: 100,
|
||||
maxElements: 10_000_000,
|
||||
}
|
||||
: options.hnswConfig,
|
||||
};
|
||||
if (distanceMetric !== undefined) {
|
||||
nativeOptions.distanceMetric = distanceMetric;
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ test('cli.js uses chalk with ESM compat', () => {
|
|||
});
|
||||
|
||||
test('package.json bin entry points to cli.js', () => {
|
||||
assert.strictEqual(packageJson.bin.ruvector, './bin/cli.js');
|
||||
assert.strictEqual(packageJson.bin.ruvector, 'bin/cli.js');
|
||||
});
|
||||
|
||||
test('package.json main entry points to dist/index.js', () => {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ try {
|
|||
assert(packageJson.version, 'Version should be set');
|
||||
assert(packageJson.main === 'dist/index.js', 'Main entry should be dist/index.js');
|
||||
assert(packageJson.types === 'dist/index.d.ts', 'Types entry should be dist/index.d.ts');
|
||||
assert(packageJson.bin.ruvector === './bin/cli.js', 'CLI bin should be ./bin/cli.js');
|
||||
assert(packageJson.bin.ruvector === 'bin/cli.js', 'CLI bin should be bin/cli.js');
|
||||
console.log(' ✓ package.json structure correct');
|
||||
|
||||
const cliExists = fs.existsSync(path.join(__dirname, '../bin/cli.js'));
|
||||
|
|
|
|||
117
npm/packages/ruvector/test/mcp-stdio.js
Normal file
117
npm/packages/ruvector/test/mcp-stdio.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Regression coverage for issues #710 and #715.
|
||||
*
|
||||
* The CLI launcher must explicitly start the required MCP module, and stdout
|
||||
* must remain exclusively JSON-RPC while the stdio transport is active.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const PACKAGE_ROOT = path.join(__dirname, '..');
|
||||
const CLI = path.join(PACKAGE_ROOT, 'bin', 'cli.js');
|
||||
const MCP_SERVER = path.join(PACKAGE_ROOT, 'bin', 'mcp-server.js');
|
||||
const LOADER_PATHS = [
|
||||
path.join(PACKAGE_ROOT, 'src', 'core', 'onnx', 'loader.js'),
|
||||
path.join(PACKAGE_ROOT, 'src', 'core', 'onnx', 'pkg', 'loader.js'),
|
||||
];
|
||||
|
||||
function waitForInitialize(child, timeoutMs = 10_000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Timed out waiting for MCP initialize response.\nstdout: ${stdout}\nstderr: ${stderr}`));
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
if (message.id === 1) {
|
||||
clearTimeout(timer);
|
||||
resolve({ message, stdout, stderr });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Preserve the output; the stdout purity assertion below reports it.
|
||||
}
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.once('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`MCP process exited before initialize: code=${code} signal=${signal}\nstderr: ${stderr}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Inspect the module export without requiring it in this test process.
|
||||
// Requiring it initializes the real intelligence worker pool, which is
|
||||
// intentionally long-lived and would keep this regression test alive.
|
||||
const serverSource = fs.readFileSync(MCP_SERVER, 'utf8');
|
||||
assert.match(
|
||||
serverSource,
|
||||
/module\.exports\s*=\s*\{[^}]*\bmain\b/,
|
||||
'mcp-server.js must export main() for the CLI launcher',
|
||||
);
|
||||
|
||||
for (const loaderPath of LOADER_PATHS) {
|
||||
const source = fs.readFileSync(loaderPath, 'utf8');
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/console\.log\((?:`Loading model:|`\s+(?:Disk )?Cache hit:|`\s+Downloading:|'Model cache cleared')/,
|
||||
`${path.relative(PACKAGE_ROOT, loaderPath)} must send diagnostics to stderr`,
|
||||
);
|
||||
}
|
||||
|
||||
const child = spawn(process.execPath, [CLI, 'mcp', 'start'], {
|
||||
cwd: PACKAGE_ROOT,
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
try {
|
||||
child.stdin.write(`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'ruvector-mcp-regression', version: '1.0.0' },
|
||||
},
|
||||
})}\n`);
|
||||
|
||||
const { message, stdout } = await waitForInitialize(child);
|
||||
assert.ok(message.result, `initialize returned an error: ${JSON.stringify(message)}`);
|
||||
|
||||
const frames = stdout.split(/\r?\n/).filter(Boolean);
|
||||
assert.ok(frames.length > 0, 'MCP server must emit an initialize response');
|
||||
for (const frame of frames) {
|
||||
assert.doesNotThrow(() => JSON.parse(frame), `stdout contained non-JSON-RPC data: ${frame}`);
|
||||
}
|
||||
} finally {
|
||||
child.stdin.end();
|
||||
child.kill('SIGTERM');
|
||||
}
|
||||
|
||||
console.log('MCP stdio launcher and stdout hygiene checks passed');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue