fix(python): resume missing immutable portfolio artifacts safely (#1205)

* fix(python): recover only missing immutable portfolio release artifacts

* docs(python): record tested public tutorial gist and immutable revision

* fix: unblock Python recovery with targeted dependency and CI repairs

* fix: patch Quinn advisories and preserve frozen inventory on Windows

* ci: verify release inventory checkout policy changes

* fix: replace unmaintained YAML parser and stabilize serving acceptance
This commit is contained in:
rUv 2026-10-13 06:04:05 -07:00 • committed by GitHub
parent 31549f7989
commit 25ed48e9e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2376 additions and 83 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Preserve the byte-pinned release inventory on every checkout.
python/portfolio/frozen-release-0.2.0.json text eol=lf

View file

@ -140,7 +140,8 @@ jobs:
# faster test discovery and execution.
test:
name: Tests (${{ matrix.name }})
runs-on: ubuntu-22.04
# ORT prebuilt binaries require glibc 2.38+ and GCC 13 libstdc++.
runs-on: ${{ matrix.name == 'core-and-rest' && 'ubuntu-24.04' || 'ubuntu-22.04' }}
# `core-and-rest` is the catch-all shard and compiles ~50 crates; on a
# cold cache the build alone has hit ~90min, so headroom matters more
# than tight feedback for this job. Faster shards still finish in ~10–20m.

View file

@ -0,0 +1,220 @@
name: Python portfolio recovery
on:
pull_request:
paths:
- '.gitattributes'
- '.github/workflows/python-portfolio-recovery.yml'
- 'python/portfolio/resume_release.py'
- 'python/portfolio/frozen-release-0.2.0.json'
- 'python/portfolio/tests/test_resume_release.py'
workflow_dispatch:
inputs:
publish:
description: 'Resume missing original 0.2.0 files after owner review; stop on any failure'
type: boolean
default: false
permissions:
contents: read
concurrency:
group: python-portfolio-recovery-${{ github.ref }}
cancel-in-progress: false
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
- run: python -m pip install pytest==9.1.1
- run: python -m pytest python/portfolio/tests/test_resume_release.py -q
prepare:
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: contract
runs-on: ${{ matrix.os }}
permissions:
contents: read
actions: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: fccf36e9a76d35bce6750dfd3b25802cae3d683b
path: frozen-source
persist-credentials: false
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
- name: Check original source, artifact and successful build gates
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
python - <<'PY'
import json, subprocess
def api(path):
return json.loads(subprocess.check_output(['gh', 'api', 'repos/ruvnet/RuVector/' + path]))
r = api('actions/runs/38169271248')
a = api('actions/artifacts/11713787141')
assert r['head_sha'] == 'fccf36e9a76d35bce6750dfd3b25802cae3d683b'
assert r['event'] == 'push' and r['path'] == '.github/workflows/python-portfolio.yml'
assert r['status'] == 'completed'
assert a['name'] == 'portfolio-dist' and not a['expired']
assert a['workflow_run']['id'] == r['id'] and a['workflow_run']['head_sha'] == r['head_sha']
assert a['digest'] == 'sha256:b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241'
jobs = api('actions/runs/38169271248/jobs?per_page=100')
assert jobs['total_count'] == len(jobs['jobs'])
for name in ('test-build', 'native-audit'):
selected = [j for j in jobs['jobs'] if j['name'] == name]
assert len(selected) == 1 and selected[0]['conclusion'] == 'success'
print('Original artifact identity and build gates verified')
PY
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
artifact-ids: '11713787141'
merge-multiple: true
github-token: ${{ github.token }}
run-id: '38169271248'
repository: ruvnet/RuVector
path: original-dist
- name: Compare all exact files before selecting missing originals
run: python python/portfolio/resume_release.py --dist original-dist --inventory python/portfolio/frozen-release-0.2.0.json --missing-dir missing-dist --receipt preflight.json
- name: Install original wheels and run frozen contracts and README examples
shell: bash
run: |
python -m venv consumer
if [[ "$RUNNER_OS" == Windows ]]; then
PY=consumer/Scripts/python.exe
else
PY=consumer/bin/python
fi
"$PY" -m pip install --upgrade pip==26.2.1
"$PY" -m pip install --index-url https://pypi.org/simple --only-binary=:all: original-dist/*.whl pytest==9.1.1 pip-audit==2.10.1
"$PY" -c 'import importlib.metadata as m; assert m.version("ruvector") == "0.1.3"'
"$PY" -m pytest frozen-source/python/portfolio/tests -q
"$PY" frozen-source/python/portfolio/check_examples.py
"$PY" -m pip_audit --local
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: recovery-preflight-${{ matrix.os }}
path: preflight.json
if-no-files-found: ignore
frozen-native-audit:
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: fccf36e9a76d35bce6750dfd3b25802cae3d683b
persist-credentials: false
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
toolchain: stable
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
- run: cargo install cargo-audit --version 0.22.2 --locked
- run: python python/portfolio/audit_native.py /tmp/frozen-native.lock
publish:
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.publish
needs: [prepare, frozen-native-audit]
runs-on: ubuntu-latest
environment: pypi
concurrency:
group: python-portfolio-pypi
cancel-in-progress: false
permissions:
contents: read
actions: read
id-token: write
env:
HAS_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN != '' }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
artifact-ids: '11713787141'
merge-multiple: true
github-token: ${{ github.token }}
run-id: '38169271248'
repository: ruvnet/RuVector
path: original-dist
- name: Reconcile registry again under the publication lock
id: select
run: |
python python/portfolio/resume_release.py --dist original-dist --inventory python/portfolio/frozen-release-0.2.0.json --missing-dir missing-dist --receipt preflight.json
python - <<'PY'
import json, os
d = json.load(open('preflight.json'))
with open(os.environ['GITHUB_OUTPUT'], 'a') as out:
out.write('missing=' + str(len(d['missing'])) + '\n')
PY
- name: Publish missing originals using Trusted Publishing
if: steps.select.outputs.missing != '0' && env.HAS_PYPI_TOKEN != 'true'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
packages-dir: missing-dist/
attestations: true
- name: Publish missing originals using the configured token
if: steps.select.outputs.missing != '0' && env.HAS_PYPI_TOKEN == 'true'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
packages-dir: missing-dist/
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
attestations: false
- name: Verify every expected file after upload
run: python python/portfolio/resume_release.py --dist original-dist --inventory python/portfolio/frozen-release-0.2.0.json --verify-only --receipt published.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: recovery-publication-receipts
path: |
preflight.json
published.json
if-no-files-found: ignore
verify-published:
needs: publish
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: fccf36e9a76d35bce6750dfd3b25802cae3d683b
persist-credentials: false
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
- name: Fresh PyPI-only installation and frozen acceptance
shell: bash
run: |
python -m venv consumer
if [[ "$RUNNER_OS" == Windows ]]; then
PY=consumer/Scripts/python.exe
else
PY=consumer/bin/python
fi
python -c 'import json,pathlib; m=json.loads(pathlib.Path("python/portfolio/manifest.json").read_text()); pathlib.Path("requirements.txt").write_text("\n".join(p+"=="+m["version"] for p in m["packages"])+"\n")'
"$PY" -m pip install --index-url https://pypi.org/simple --only-binary=:all: --no-cache-dir -r requirements.txt pytest==9.1.1
"$PY" -c 'import importlib.metadata as m; assert m.version("ruvector") == "0.1.3"'
"$PY" -m pytest python/portfolio/tests -q
"$PY" python/portfolio/check_examples.py

View file

@ -92,6 +92,9 @@ jobs:
- run: cargo install cargo-audit --version 0.22.2 --locked
- run: python python/portfolio/audit_native.py /tmp/portfolio-native.lock
publish:
concurrency:
group: python-portfolio-pypi
cancel-in-progress: false
needs: [test-build, native-audit]
if: needs.test-build.outputs.release == 'true'
runs-on: ubuntu-latest

81
Cargo.lock generated
View file

@ -159,6 +159,16 @@ dependencies = [
"rayon",
]
[[package]]
name = "annotate-snippets"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96401ca08501972288ecbcde33902fce858bf73fbcbdf91dab8c3a9544e106bb"
dependencies = [
"anstyle",
"unicode-width 0.2.0",
]
[[package]]
name = "anstream"
version = "0.6.21"
@ -274,6 +284,12 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.9"
@ -3968,6 +3984,16 @@ dependencies = [
"bitflags 2.13.0",
]
[[package]]
name = "granit-parser"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0"
dependencies = [
"arraydeque",
"smallvec",
]
[[package]]
name = "grep-matcher"
version = "0.1.8"
@ -8085,14 +8111,15 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.15"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
dependencies = [
"bytes",
"getrandom 0.3.4",
"getrandom 0.4.3",
"lru-slab",
"rand 0.9.4",
"rand 0.10.1",
"rand_pcg 0.10.2",
"ring",
"rustc-hash 2.1.2",
"rustls",
@ -8172,7 +8199,7 @@ dependencies = [
"rand_isaac",
"rand_jitter",
"rand_os",
"rand_pcg",
"rand_pcg 0.1.2",
"rand_xorshift 0.1.1",
"winapi",
]
@ -8361,6 +8388,15 @@ dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "rand_xorshift"
version = "0.1.1"
@ -12064,8 +12100,8 @@ dependencies = [
"rvagent-backends",
"rvagent-core",
"serde",
"serde-saphyr",
"serde_json",
"serde_yaml",
"sha3",
"smallvec",
"tempfile",
@ -12417,6 +12453,20 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-saphyr"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8"
dependencies = [
"annotate-snippets",
"encoding_rs_io",
"granit-parser",
"num-traits",
"serde_core",
"smallvec",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
@ -12512,19 +12562,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.12.1",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "seti-boundary-discovery"
version = "0.1.0"
@ -14572,12 +14609,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"

View file

@ -217,17 +217,19 @@ impl ReplicaSet {
/// Promote a secondary to primary
pub fn promote_to_primary(&mut self, id: &str) -> Result<()> {
// Get the replica and verify it exists
let mut replica = self
// Release the shard guard before accessing the old primary: both
// keys can share a shard and nested access would deadlock.
let role = self
.replicas
.get_mut(id)
.ok_or_else(|| ReplicationError::ReplicaNotFound(id.to_string()))?;
.get(id)
.ok_or_else(|| ReplicationError::ReplicaNotFound(id.to_string()))?
.role;
if replica.role == ReplicaRole::Primary {
if role == ReplicaRole::Primary {
return Ok(());
}
if replica.role == ReplicaRole::Witness {
if role == ReplicaRole::Witness {
return Err(ReplicationError::InvalidState(
"Cannot promote witness to primary".to_string(),
));
@ -245,8 +247,11 @@ impl ReplicaSet {
}
}
// Promote new primary
replica.role = ReplicaRole::Primary;
// Exclusive &mut self prevents membership changes during promotion.
self.replicas
.get_mut(id)
.expect("validated replica remains in the set")
.role = ReplicaRole::Primary;
let mut primary = self.primary_id.write();
*primary = Some(id.to_string());
@ -362,6 +367,48 @@ mod tests {
assert_eq!(primary.role, ReplicaRole::Primary);
}
#[test]
fn test_promotion_same_shard_completes() {
// Find a real collision without depending on DashMap's random seed.
let mut set = ReplicaSet::new("collision");
set.replicas = Arc::new(DashMap::with_shard_amount(2));
for i in 0..3 {
set.add_replica(format!("r{i}"), "127.0.0.1:0", ReplicaRole::Secondary)
.unwrap();
}
let mut pair = None;
for i in 0..3 {
let id = format!("r{i}");
let guard = set.replicas.get_mut(&id).unwrap();
for j in (i + 1)..3 {
let candidate = format!("r{j}");
if matches!(
set.replicas.try_get_mut(&candidate),
dashmap::try_result::TryResult::Locked
) {
pair = Some((id.clone(), candidate));
break;
}
}
drop(guard);
if pair.is_some() {
break;
}
}
let (old, new) = pair.expect("three keys in two shards must collide");
set.promote_to_primary(&old).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
set.promote_to_primary(&new).unwrap();
assert_eq!(set.get_primary().unwrap().id, new);
assert_eq!(set.get_replica(&old).unwrap().role, ReplicaRole::Secondary);
set.promote_to_primary(&new).unwrap(); // idempotent
tx.send(()).unwrap();
});
rx.recv_timeout(Duration::from_secs(5))
.expect("promotion must not deadlock on colliding shard keys");
}
#[test]
fn test_quorum() {
let mut set = ReplicaSet::new("cluster-1");

View file

@ -193,6 +193,11 @@ pub async fn run(
.parse::<SocketAddr>()
.context("Invalid address")?;
// Bind before announcing readiness; clients can connect upon the banner.
// Report the actual port when zero was requested.
let listener = tokio::net::TcpListener::bind(addr).await?;
let addr = listener.local_addr()?;
println!();
if mock_mode {
println!(
@ -221,7 +226,6 @@ pub async fn run(
println!();
// Set up graceful shutdown
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await

View file

@ -167,9 +167,11 @@ fn allow_mock_serves_labeled_placeholder_responses() {
.unwrap();
let mut server = Server(child);
let stdout = BufReader::new(server.0.stdout.take().unwrap());
// Keep the pipe open until requests finish: dropping it at the banner
// can make the child panic on its remaining stdout writes.
let mut stdout = BufReader::new(server.0.stdout.take().unwrap());
let mut banner = String::new();
for line in stdout.lines() {
for line in stdout.by_ref().lines() {
let line = line.unwrap();
banner.push_str(&line);
banner.push('\n');

View file

@ -2,6 +2,7 @@
name = "rvagent-middleware"
version = "0.1.0"
edition = "2021"
rust-version = "1.89"
description = "rvAgent middleware — pipeline, todolist, filesystem, subagents, summarization, memory, skills, prompt caching, HITL, witness, tool sanitizer"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/RuVector"
@ -16,7 +17,7 @@ rvagent-core = { version = "0.1.0", path = "../rvagent-core" }
rvagent-backends = { version = "0.1.0", path = "../rvagent-backends" }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
serde-saphyr = { version = "=1.3.0", default-features = false, features = ["deserialize"] }
tokio = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View file

@ -98,6 +98,95 @@ fn truncate(s: &str, max_len: usize) -> String {
}
}
// Retain only the scalar strings and string-keyed maps used by skill metadata.
// Other YAML values are consumed without coercing numeric keys into strings.
enum FrontmatterValue {
String(String),
Mapping(HashMap<String, FrontmatterValue>),
Other,
}
impl FrontmatterValue {
fn as_str(&self) -> Option<&str> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
fn as_mapping(&self) -> Option<&HashMap<String, Self>> {
match self {
Self::Mapping(value) => Some(value),
_ => None,
}
}
}
impl<'de> Deserialize<'de> for FrontmatterValue {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = FrontmatterValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("YAML skill frontmatter")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(FrontmatterValue::String(value.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
Ok(FrontmatterValue::String(value))
}
fn visit_bool<E: serde::de::Error>(self, _: bool) -> Result<Self::Value, E> {
Ok(FrontmatterValue::Other)
}
fn visit_i64<E: serde::de::Error>(self, _: i64) -> Result<Self::Value, E> {
Ok(FrontmatterValue::Other)
}
fn visit_u64<E: serde::de::Error>(self, _: u64) -> Result<Self::Value, E> {
Ok(FrontmatterValue::Other)
}
fn visit_f64<E: serde::de::Error>(self, _: f64) -> Result<Self::Value, E> {
Ok(FrontmatterValue::Other)
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(FrontmatterValue::Other)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut sequence: A,
) -> Result<Self::Value, A::Error> {
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(FrontmatterValue::Other)
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut strings = HashMap::new();
while let Some((key, value)) =
map.next_entry::<FrontmatterValue, FrontmatterValue>()?
{
if let FrontmatterValue::String(key) = key {
strings.insert(key, value);
}
}
Ok(FrontmatterValue::Mapping(strings))
}
}
deserializer.deserialize_any(Visitor)
}
}
/// Parse skill metadata from YAML frontmatter in a SKILL.md file.
///
/// Returns `None` if the file is too large, has no frontmatter, or frontmatter is invalid.
@ -135,19 +224,26 @@ pub fn parse_skill_metadata(
return None;
}
let frontmatter: serde_yaml::Value = serde_yaml::from_str(frontmatter_str).ok()?;
// Pure-Rust YAML parsing; filesystem includes and property expansion stay disabled.
let mut budget = serde_saphyr::Budget::default();
budget.max_depth = 32;
budget.max_documents = 1;
budget.max_aliases = 64;
budget.max_anchors = 64;
budget.max_nodes = 4096;
budget.max_events = 8192;
budget.max_total_scalar_bytes = 64 * 1024;
let mut options = serde_saphyr::Options::default();
options.budget = Some(budget);
// Match serde_yaml::Value: merge keys are data unless explicitly applied.
options.merge_keys = serde_saphyr::MergeKeyPolicy::AsOrdinary;
options.strict_booleans = true;
let frontmatter: FrontmatterValue =
serde_saphyr::from_str_with_options(frontmatter_str, options).ok()?;
let map = frontmatter.as_mapping()?;
let name = map
.get(&serde_yaml::Value::String("name".into()))?
.as_str()?
.trim()
.to_string();
let description = map
.get(&serde_yaml::Value::String("description".into()))?
.as_str()?
.trim()
.to_string();
let name = map.get("name")?.as_str()?.trim().to_string();
let description = map.get("description")?.as_str()?.trim().to_string();
// Validate skill name (warn but continue for backwards compatibility)
if let Err(err) = validate_skill_name(&name, directory_name) {
@ -161,7 +257,7 @@ pub fn parse_skill_metadata(
// Parse allowed-tools (space-delimited string, strip commas)
let allowed_tools = map
.get(&serde_yaml::Value::String("allowed-tools".into()))
.get("allowed-tools")
.and_then(|v| v.as_str())
.map(|s| {
s.split_whitespace()
@ -172,21 +268,21 @@ pub fn parse_skill_metadata(
.unwrap_or_default();
let license = map
.get(&serde_yaml::Value::String("license".into()))
.get("license")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string());
let compatibility = map
.get(&serde_yaml::Value::String("compatibility".into()))
.get("compatibility")
.and_then(|v| v.as_str())
.map(|s| truncate(s.trim(), MAX_SKILL_COMPATIBILITY_LENGTH));
let metadata = map
.get(&serde_yaml::Value::String("metadata".into()))
.get("metadata")
.and_then(|v| v.as_mapping())
.map(|m| {
m.iter()
.filter_map(|(k, v)| Some((k.as_str()?.to_string(), v.as_str()?.to_string())))
.filter_map(|(k, v)| Some((k.to_string(), v.as_str()?.to_string())))
.collect()
})
.unwrap_or_default();
@ -448,4 +544,62 @@ mod tests {
assert_eq!(meta.metadata.get("version"), Some(&"1.0".to_string()));
assert_eq!(meta.metadata.get("author"), Some(&"test".to_string()));
}
#[test]
fn test_yaml_metadata_preserves_string_only_filtering() {
let content = "---\nname: skill\ndescription: Test\nmetadata:\n 4: ignored\n good: value\n count: 4\n flag: true\n items: [one, two]\nunknown: { [a, b]: ignored }\n---\n";
let meta = parse_skill_metadata(content, "path", "skill").unwrap();
assert_eq!(
meta.metadata,
HashMap::from([("good".into(), "value".into())])
);
}
#[test]
fn test_yaml_alias_and_block_scalar() {
let content = "---\nname: skill\ndescription: &description >-\n useful\n skill\nmetadata:\n copy: *description\n---\n";
let meta = parse_skill_metadata(content, "path", "skill").unwrap();
assert_eq!(meta.description, "useful skill");
assert_eq!(meta.metadata["copy"], "useful skill");
}
#[test]
fn test_yaml_rejects_duplicate_and_malformed_frontmatter() {
for yaml in [
"name: skill\nname: other\ndescription: Test",
"name: [broken",
"name: 5\ndescription: Test",
] {
assert!(
parse_skill_metadata(&format!("---\n{yaml}\n---\n"), "path", "skill").is_none()
);
}
}
#[test]
fn test_yaml_rejects_excessive_nesting_and_aliases() {
let nested = format!(
"---\nname: skill\ndescription: Test\nignored: {}0{}\n---\n",
"[".repeat(40),
"]".repeat(40)
);
assert!(parse_skill_metadata(&nested, "path", "skill").is_none());
let aliases = format!(
"---\nname: skill\ndescription: &a Test\nignored: [{}]\n---\n",
vec!["*a"; 65].join(",")
);
assert!(parse_skill_metadata(&aliases, "path", "skill").is_none());
}
#[test]
fn test_yaml_merge_keys_remain_ordinary_data() {
let content = "---\nbase: &base {name: skill, description: Test}\n<<: *base\n---\n";
assert!(parse_skill_metadata(content, "path", "skill").is_none());
let content = "---\nname: skill\ndescription: Test\nmetadata: {<<: {hidden: ignored}, visible: yes}\n---\n";
let meta = parse_skill_metadata(content, "path", "skill").unwrap();
assert_eq!(
meta.metadata,
HashMap::from([("visible".into(), "yes".into())])
);
}
}

View file

@ -0,0 +1,76 @@
# Portfolio recovery evidence, 2026-10-12
This repair addresses the incomplete 0.2.0 release before adding another Python
library. It does not rebuild or replace immutable published versions.
## Observed state
The original supervisor stopped after a publication failure. PR1188 merged as
`fccf36e9a76d35bce6750dfd3b25802cae3d683b`. The actual main release
[run38169271248](https://github.com/ruvnet/RuVector/actions/runs/38169271248)
passed build and native audit, then stopped on PyPI HTTP429: too many new projects.
On October12, two original wheels are present and match their frozen SHA256:
attention and cluster0.2.0. Their source archives and nine other complete target
releases remain missing: 20 of22 distribution files are absent. This is not a
completed portfolio release. The checked in inventory records exact registry
filenames and hashes; no token or private customer data is included.
The original runtime is0.1.3. Main now requests0.1.4, so rebuilding0.2.0 from main
would produce different dependency metadata. Recovery uses only the original
archive, artifact11713787141, SHA256
`b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241`.
The actual downloaded ZIP and all22 extracted file hashes were verified anew.
## Validation
| Check | Observed result |
| --- | --- |
| Recovery adversarial contracts |62 passed on Linux; Windows permission behavior simulated, not a native Windows run |
| Original installed wheel contracts |103 passed, CPython3.12.3 Linuxx86_64 |
| Original README examples |11 passed against installed original artifacts |
| Published attention tutorial |Passed from fresh PyPI installation: attention0.2.0, runtime0.1.3, NumPy2.2.6 |
| Workflow validation |actionlint1.7.7 passed; official release binary checksum verified; external shellcheck/pyflakes integrations disabled |
| Python dependency audit |pip-audit2.10.1 reported no known issues in resolved dependencies; nine unpublished companion distributions could not be looked up |
| Read-only source scan |Pinned Ruflo3.25.6 reported zero findings for the recovery helper; no advisory feed timestamp supplied |
| Secret scan |detect-secrets1.5.0 flagged four public provenance hashes, manually confirmed false positives; no credentials present |
| Independent review |Approved exact helper and workflow after correcting Windows mode handling and artifact download directory layout |
| Live registry preflight |Two existing hashes match,20 missing original files selected, zero uploads |
The native audit from the original run is preserved as historical evidence.
Recovery additionally gates upload on a fresh frozen-source native audit, fresh
original artifact installation on Linux/macOS/Windows, and dependency checks.
Hosted platform gates and post-publication PyPI consumer tests remain to run.
## Failure and promotion policy
PR and default dispatch checks cannot publish. An explicit main dispatch with
`publish=true` enters the existing protected pypi environment only after
platform and audit gates pass. Ordinary and recovery portfolio publishers share
one concurrency group. Under that lock the registry is checked again; existing
matching files are skipped by selection, never by trusting a duplicate-upload
error. Hash conflicts, yanked files, metadata mismatches, unsafe paths and registry
errors stop selection.
There is no established upload429 cooldown in official guidance. The24hour rule
in [Trusted Publisher guidance](https://docs.pypi.org/trusted-publishers/troubleshooting/)
concerns publisher registration, not this upload error. No rapid retry, alternate
account or IP workaround is configured. A failed upload leaves a partial registry
state and requires a new read-only inventory before another bounded attempt.
Disabling the recovery entry point rolls back execution without deleting or
overwriting published versions. The exact file ledger and receipts stay available.
Windows relies on the trusted hosted runner ACL rather than POSIX permission bits.
Checksums detect changes; they do not replace protected source review.
## Candidate decision
[candidate-decision.json](candidate-decision.json) records the requested weighted
ranking and fresh npm window September12 through October11. Filtered retrieval
is the leading future extension of an existing package; no new library is
selected while the original release is incomplete. Counts are downloads, not
unique users, and usefulness/readiness scores are engineering estimates.
Public tested tutorial: https://gist.github.com/ruvnet/2491c7631bf3e186e815196f52fd8954
The posted code was fetched back from GitHub, matched byte for byte and ran successfully. See tutorial-ledger.json for its immutable revision and code digest.

View file

@ -0,0 +1,76 @@
{
"date": "2026-10-12",
"source_sha": "31549f798997cbb16b1ec872ca30b9e690bbd3ff",
"download_window": {
"start": "2026-09-12",
"end": "2026-10-11"
},
"weights": {
"customer_usefulness": 0.4,
"npm_demand": 0.25,
"implementation_readiness": 0.2,
"maintenance_ease": 0.15
},
"method": "Scores 0..5 are prioritization estimates, not measured customer outcomes. Demand uses log(downloads+1) relative to the largest observed candidate package. Core downloads are a package proxy, not feature-specific demand. Downloads are not unique users.",
"ranking": [
{
"candidate": "Filtered retrieval extension",
"demand_package": "@ruvector/core",
"npm_downloads": 332090,
"source_paths": "crates/ruvector-py/src/lib.rs; crates/ruvector-core/src/advanced_features/filtered_search.rs",
"scores": {
"customer_usefulness": 5,
"npm_demand": 4.859,
"implementation_readiness": 5,
"maintenance_ease": 4
},
"weighted_score": 4.815,
"exclusion": "Extend existing core after recovery; nativeCollection already implementsmetadatafilteredretrieval"
},
{
"candidate": "RVF import/export extension",
"demand_package": "@ruvector/rvf",
"npm_downloads": 480813,
"source_paths": "crates/rvf/; native prerequisite PR1190",
"scores": {
"customer_usefulness": 5,
"npm_demand": 5.0,
"implementation_readiness": 2,
"maintenance_ease": 4
},
"weighted_score": 4.25,
"exclusion": "Export/metadataAPI proposal unmerged; verify versionidentity beforeextendingexistingRVF"
},
{
"candidate": "Sparse/dense hybrid retrieval",
"demand_package": "@ruvector/core",
"npm_downloads": 332090,
"source_paths": "crates/ruvector-core/src/advanced_features/hybrid_search.rs; sparse_vector.rs",
"scores": {
"customer_usefulness": 4,
"npm_demand": 4.859,
"implementation_readiness": 2,
"maintenance_ease": 3
},
"weighted_score": 3.665,
"exclusion": "Rustsourceexists; inspectbinding/capabilitymatrix beforeselection"
},
{
"candidate": "KGE link prediction",
"demand_package": "@ruvector/kge",
"npm_downloads": 422,
"source_paths": "crates/ruvector-kge-wasm/src/",
"scores": {
"customer_usefulness": 4,
"npm_demand": 2.311,
"implementation_readiness": 2,
"maintenance_ease": 2
},
"weighted_score": 2.878,
"exclusion": "422monthlydownloads; PythonAPI/exportreadiness unverified; avoid duplicatepackage"
}
],
"selection": null,
"decision": "REPAIR_FIRST_NO_NEW_LIBRARY",
"reason": "Nine target companion0.2.0 releases absent and20originalfilesmissing. Newprojectupload429 unresolved; existingunmergedintegration/nativeAPIprerequisites alreadyconsumeattention. Repairfrozenportfolioanddeliverpublishedattentiontutorial; no newcandidatebranch/package."
}

View file

@ -0,0 +1,26 @@
{
"retrieved_at": "2026-10-12T06:47:51.149881+00:00",
"counts": [
{
"downloads": 332090,
"start": "2026-09-12",
"end": "2026-10-11",
"package": "@ruvector/core",
"source_url": "https://api.npmjs.org/downloads/point/2026-09-12:2026-10-11/@ruvector/core"
},
{
"downloads": 480813,
"start": "2026-09-12",
"end": "2026-10-11",
"package": "@ruvector/rvf",
"source_url": "https://api.npmjs.org/downloads/point/2026-09-12:2026-10-11/@ruvector/rvf"
},
{
"downloads": 422,
"start": "2026-09-12",
"end": "2026-10-11",
"package": "@ruvector/kge",
"source_url": "https://api.npmjs.org/downloads/point/2026-09-12:2026-10-11/@ruvector/kge"
}
]
}

View file

@ -0,0 +1,62 @@
{
"archive_sha256": "b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241",
"files": {
"ruvector_attention-0.2.0-py3-none-any.whl": "48c6557e4fc53c41aeb24b832e3e167afe88fde6c17affbb31cd6da148972bdc",
"ruvector_attention-0.2.0.tar.gz": "767881ffe1e0023ed2866ccbb57fdab1c985149ec89d1c003e2e737d7327aa70",
"ruvector_cluster-0.2.0-py3-none-any.whl": "99efcf69e29c9a11e47599ea37a725496e11fa88b169224885e23efa0d6ec3b8",
"ruvector_cluster-0.2.0.tar.gz": "09e0db4fc6f19a6b0e45b7906c5647adb24917dd5a3b2fa59a282a3629333635",
"ruvector_core-0.2.0-py3-none-any.whl": "c269a9cd41dbf6b91900e1709f31e1576560b51e70a553adfeb219d108c1cd89",
"ruvector_core-0.2.0.tar.gz": "24a62d2a0b08f4a4a5f1470922f04c8727a75ea36fb4aea3691d087f0d3c5630",
"ruvector_gnn-0.2.0-py3-none-any.whl": "4e5e013e3ba87205d2acaabaf1420ada8a37cb9bb8281168395c741c5c6eedcb",
"ruvector_gnn-0.2.0.tar.gz": "46706f979d8d45a9d0eb19005f595aa2fe48d88804d04e99acbe22f139a6d100",
"ruvector_graph-0.2.0-py3-none-any.whl": "2278acfa6255df9bd961c80ebb36fa28621975b36eb7fb41d992b7b5a921b8cc",
"ruvector_graph-0.2.0.tar.gz": "5410b43357250b3719f8c7f700b4cad9e99242bc53532141085ebfb08565ccde",
"ruvector_rerank-0.2.0-py3-none-any.whl": "5990bbcc2fabab087480740770f240f10bf65c2bf6400aba6911c27079a9cea4",
"ruvector_rerank-0.2.0.tar.gz": "839921d2d7d14387cc572bb210cc356c23b84d2946706dc7432e87afb1bcf87c",
"ruvector_router-0.2.0-py3-none-any.whl": "7066ec5b35e3cfb1f5dbb902b87bed18aa85ad10b76f085137e823b5ac0e4938",
"ruvector_router-0.2.0.tar.gz": "10c79f3a0f7c86820e7128b9a1e3d426b66f22f248a7eb2a1c92ba891a02eac5",
"ruvector_ruvllm-0.2.0-py3-none-any.whl": "ccad8d79a43811148908a97e161f2eebcadbe1ab8975aaa266a0824c3d3cc720",
"ruvector_ruvllm-0.2.0.tar.gz": "860a882d5403789c9da73f51e9177a79139eb66617627cb454982da39e4fed4a",
"ruvector_rvf-0.2.0-py3-none-any.whl": "7003a7866227bc4ad336c77f677456752c4bc0f6acf37843c6effd94749d496e",
"ruvector_rvf-0.2.0.tar.gz": "4d2902065f1212ffbc4861bc316dbc9f7b928397e0840682e8d8ef246527624f",
"ruvector_rvf_wasm-0.2.0-py3-none-any.whl": "17bb6bbdb86595c9644a2cac6cb2e631fa2341eb9f87235f2105324aef989e57",
"ruvector_rvf_wasm-0.2.0.tar.gz": "566d5d46365188e1493cdace52020f5d9da38232b79580a6b946e549d8cd8028",
"ruvector_sona-0.2.0-py3-none-any.whl": "bbf005d0438458a6d1dfdd78e9553eee9cda7933af059450ca7202188eafa4ce",
"ruvector_sona-0.2.0.tar.gz": "23db0420e9b1eff2d8c01a61eb84dd1f97e46787a9b0a0ac73cf3908458cd273"
},
"missing": [
"ruvector_attention-0.2.0.tar.gz",
"ruvector_cluster-0.2.0.tar.gz",
"ruvector_core-0.2.0-py3-none-any.whl",
"ruvector_core-0.2.0.tar.gz",
"ruvector_gnn-0.2.0-py3-none-any.whl",
"ruvector_gnn-0.2.0.tar.gz",
"ruvector_graph-0.2.0-py3-none-any.whl",
"ruvector_graph-0.2.0.tar.gz",
"ruvector_rerank-0.2.0-py3-none-any.whl",
"ruvector_rerank-0.2.0.tar.gz",
"ruvector_router-0.2.0-py3-none-any.whl",
"ruvector_router-0.2.0.tar.gz",
"ruvector_ruvllm-0.2.0-py3-none-any.whl",
"ruvector_ruvllm-0.2.0.tar.gz",
"ruvector_rvf-0.2.0-py3-none-any.whl",
"ruvector_rvf-0.2.0.tar.gz",
"ruvector_rvf_wasm-0.2.0-py3-none-any.whl",
"ruvector_rvf_wasm-0.2.0.tar.gz",
"ruvector_sona-0.2.0-py3-none-any.whl",
"ruvector_sona-0.2.0.tar.gz"
],
"operation": "precheck-only",
"promotion": "independent-review-and-protected-environment-required",
"published": [
"ruvector_attention-0.2.0-py3-none-any.whl",
"ruvector_cluster-0.2.0-py3-none-any.whl"
],
"runtime": "0.1.3",
"schema": 1,
"source_artifact": 11713787141,
"source_run": 38169271248,
"source_sha": "fccf36e9a76d35bce6750dfd3b25802cae3d683b",
"status": "validated",
"version": "0.2.0"
}

View file

@ -0,0 +1,95 @@
[
{
"package": "ruvector",
"latest": "0.1.4",
"target": "0.1.4",
"files": [
{
"name": "ruvector-0.1.4-cp39-abi3-macosx_10_12_x86_64.whl",
"sha256": "3da743e96235ef90d2b95e10f13b42da81374f1844daf3d1fabb9ab011b8670d"
},
{
"name": "ruvector-0.1.4-cp39-abi3-macosx_11_0_arm64.whl",
"sha256": "28aeac25bb3151b4afb0b06bc07ec259137432387c7c2cb3316842bc55e82006"
},
{
"name": "ruvector-0.1.4-cp39-abi3-manylinux_2_28_aarch64.whl",
"sha256": "d6dabb8f3d5add92e8721adcfc28dcaaaadb70e62ac7e729c625ed3870497a7c"
},
{
"name": "ruvector-0.1.4-cp39-abi3-manylinux_2_28_x86_64.whl",
"sha256": "2189bf262fab1533f40ac654afa4bcb580b87ad2e33eee1decd0b7a42f469ebe"
},
{
"name": "ruvector-0.1.4-cp39-abi3-win_amd64.whl",
"sha256": "6032dd22b1f74d0d4fa8c23142f88ca9275bcb5f31cd114c3e875e164356d5ee"
},
{
"name": "ruvector-0.1.4.tar.gz",
"sha256": "614697f60b038486372fdd7f6feef819f89938b955b38e7fe5fb608d14406e5f"
}
]
},
{
"package": "ruvector-attention",
"latest": "0.2.0",
"target": "0.2.0",
"files": [
{
"name": "ruvector_attention-0.2.0-py3-none-any.whl",
"sha256": "48c6557e4fc53c41aeb24b832e3e167afe88fde6c17affbb31cd6da148972bdc"
}
]
},
{
"package": "ruvector-cluster",
"latest": "0.2.0",
"target": "0.2.0",
"files": [
{
"name": "ruvector_cluster-0.2.0-py3-none-any.whl",
"sha256": "99efcf69e29c9a11e47599ea37a725496e11fa88b169224885e23efa0d6ec3b8"
}
]
},
{
"package": "ruvector-core",
"status": 404
},
{
"package": "ruvector-gnn",
"status": 404
},
{
"package": "ruvector-graph",
"latest": "0.1.0",
"target": "0.2.0",
"files": []
},
{
"package": "ruvector-rerank",
"latest": "0.1.0",
"target": "0.2.0",
"files": []
},
{
"package": "ruvector-router",
"status": 404
},
{
"package": "ruvector-ruvllm",
"status": 404
},
{
"package": "ruvector-rvf",
"status": 404
},
{
"package": "ruvector-rvf-wasm",
"status": 404
},
{
"package": "ruvector-sona",
"status": 404
}
]

View file

@ -0,0 +1,19 @@
{
"date": "2026-10-12",
"package": "ruvector-attention",
"version": "0.2.0",
"native": "ruvector==0.1.3",
"numpy": "2.2.6",
"gist_url": "https://gist.github.com/ruvnet/2491c7631bf3e186e815196f52fd8954",
"gist_id": "2491c7631bf3e186e815196f52fd8954",
"gist_revision": "7c09fe56274683c0b838ed0a2b176abdd86ff079",
"script_sha256": "0d9961c2c93a1cec8debcd744aeacc141c662042042e7646e1f8a1a14a61ad0f",
"status": "published package tutorial; tested on Linux; broader portfolio incomplete",
"source_sha": "fccf36e9a76d35bce6750dfd3b25802cae3d683b",
"recovery_pr": "https://github.com/ruvnet/RuVector/pull/1205",
"acceptance": {
"platform": "Linuxx86_64",
"python": "3.10.22",
"result": "PASS native attention matches independent reference and invalidinputs rejected"
}
}

View file

@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1040 330" role="img" aria-labelledby="recovery-title recovery-desc">
<title id="recovery-title">Recover the original RuVector Python portfolio release</title>
<desc id="recovery-desc">The frozen 0.2.0 release and its checksum ledger are compared with the current PyPI inventory. Only missing original files may advance through reviewed protected publishing. A mismatch or HTTP 429 stops promotion. Animation is decorative; all information remains visible when reduced motion is enabled.</desc>
<style>
.label{font:600 17px system-ui,sans-serif;fill:#edf5ff}
.detail{font:14px system-ui,sans-serif;fill:#b8cadf}
.flow{stroke:#55d8c4;stroke-width:2;stroke-dasharray:6 8;animation:flow 1.8s linear infinite}
.pulse{animation:pulse 3s ease-in-out infinite}
@keyframes flow{to{stroke-dashoffset:-28}}
@keyframes pulse{0%,100%{opacity:.55}50%{opacity:1}}
@media(prefers-reduced-motion:reduce){.flow,.pulse{animation:none}}
</style>
<rect width="1040" height="330" rx="20" fill="#101a2b"/>
<text x="32" y="46" class="label" style="font-size:25px">Recover the original release</text>
<text x="32" y="73" class="detail">Companions 0.2.0 · frozen native runtime 0.1.3 · one publisher owner</text>
<path d="M206 160H248 M408 160H450 M610 160H652 M812 160H854" class="flow"/>
<g fill="#192a41" stroke="#46708a">
<rect x="32" y="112" width="174" height="97" rx="12"/>
<rect x="248" y="112" width="160" height="97" rx="12"/>
<rect x="450" y="112" width="160" height="97" rx="12"/>
<rect x="652" y="112" width="160" height="97" rx="12"/>
<rect x="854" y="112" width="154" height="97" rx="12"/>
</g>
<g class="label">
<text x="47" y="142">Frozen artifact</text>
<text x="264" y="142">Compare PyPI</text>
<text x="466" y="142">Missing files</text>
<text x="668" y="142">Review gate</text>
<text x="870" y="142">Verify</text>
</g>
<g class="detail">
<text x="47" y="171">22 original files</text>
<text x="47" y="193">SHA256 ledger</text>
<text x="264" y="171">Name + version</text>
<text x="264" y="193">Existing hashes</text>
<text x="466" y="171">Original bytes</text>
<text x="466" y="193">No overwrite</text>
<text x="668" y="171">Protected PyPI</text>
<text x="668" y="193">Required checks</text>
<text x="870" y="171">Fresh inventory</text>
<text x="870" y="193">Consumer tests</text>
</g>
<rect x="32" y="239" width="976" height="57" rx="10" fill="#2c2634" stroke="#bf7c91"/>
<circle cx="54" cy="267" r="6" fill="#ffb2c4" class="pulse"/>
<text x="73" y="262" class="label" style="fill:#ffcbda">Mismatch or HTTP 429: stop promotion</text>
<text x="73" y="284" class="detail">Reinspect live state before retrying. Publication and installation verification are separate milestones.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,80 @@
# ADR: Recover the frozen Python portfolio release
Status: Proposed
Date: October 12, 2026
Scope: RuVector companion distributions at version 0.2.0
![Release recovery overview](recovery-2026-10-12.svg)
## Context
The initial portfolio release partially reached PyPI. A new-project HTTP 429 interrupted publication. Package names, versions and actual published files must be inspected again before any upload. A successful wheel upload does not imply its source distribution or another package was uploaded.
The original companion artifacts require `ruvector==0.1.3`. Current main changes the same companion version to depend on runtime 0.1.4. Rebuilding 0.2.0 from current main would silently mix release identities and cannot repair the original release faithfully.
The stopped historical supervisor had a deadline ending October 11 at 22:35 UTC. Its receipt is useful evidence, not proof that no other publisher is running. Recovery must establish one current publisher owner from live process state, workflow runs, private Slack claims and existing receipts.
## Decision
Recover only the original immutable artifacts. Do not add another Python library while this portfolio delivery is unfinished.
The frozen release identity is:
| Field | Exact value |
| --- | --- |
| Source commit | `fccf36e9a76d35bce6750dfd3b25802cae3d683b` |
| Companion version | `0.2.0` |
| Required native runtime | `0.1.3` |
| GitHub Actions run | `38169271248` |
| Artifact ID | `11713787141` |
| Artifact archive SHA256 | `b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241` |
| Expected companion artifacts | 22 files across 11 distributions |
The [frozen inventory](../../python/portfolio/frozen-release-0.2.0.json) records each expected filename, byte size and SHA256. It provides release identity; it does not authorize publication.
A recovery implementation must:
1. Confirm no live publisher already owns this release; claim the scoped branch and package work in the existing private Slack thread.
2. Download the original artifact by immutable run and artifact identity. Verify the archive checksum and all 22 member checksums against the committed inventory. Reject path traversal, extra members, missing members and mismatched bytes.
3. Fetch the current PyPI JSON for every expected distribution/version. An existing expected filename must match its frozen SHA256. Unexpected release filenames or mismatches stop recovery.
4. Stage only files missing from PyPI. Stage nothing when all expected files are already present. Avoid `skip-existing` as a substitute for preflight identity verification.
5. Require independent review and repository release checks before using the protected `pypi` environment. Publish through the established GitHub Actions identity without broadening secrets, changing protection or bypassing approval.
6. Stop on HTTP 429. There is no confirmed upload cooldown here. Do not infer that the separate documented Trusted Publisher registration limit applies to this upload. Reinspect state and receipts before a later authorized attempt.
7. Requery PyPI after publication, verify every expected filename and hash, and execute installed-artifact consumer tests. Record implementation, checks, merge, publication and verification separately.
A transport failure or ambiguous upload response requires read-only reconciliation first. A manifest cannot establish whether a network side effect completed.
## Architecture
```mermaid
flowchart TD
A["Frozen artifact and ledger"] --> B["Archive and file verification"]
C["Current PyPI inventory"] --> D["Identity and missing file check"]
B --> D
D --> E{"Safe to publish?"}
E -->|Mismatch or 429| F["Stop and record blocker"]
E -->|Reviewed missing files| G["Protected publishing"]
G --> H["PyPI and consumer verification"]
```
## Tutorial acceptance
The companion [attention tutorial](../../python/portfolio/tutorials/README.md) uses only published `ruvector-attention==0.2.0`, runtime `ruvector==0.1.3` and NumPy 2.2.6. Its exact code passed a clean PyPI install on CPython 3.10.22, Linux x86_64 and glibc 2.39 on October 12, 2026. It checks the native weighted sum against a float64 reference and rejects invalid dimensions and nonfinite queries.
This smoke test does not prove all portfolio files are published, benchmark performance or establish fresh macOS/Windows execution. The example does not download models, generate text or train parameters.
## Consequences and alternatives
Frozen-file recovery preserves original published wheel metadata and avoids duplicate version content. Original artifacts must remain available and independently verified. If the archive becomes unavailable or its checksum differs, recovery stops; a reviewed new version is preferable to rebuilding historical bytes.
Rebuilding from current main was rejected because the runtime dependency changed without changing the companion version. Blindly repeating the supervisor was rejected because it could duplicate a publisher and obscure partial side effects. Ignoring HTTP 429 or modifying credentials was rejected because neither repairs the release gate.
Rollback means stopping promotion and preserving the recorded inventory. PyPI files are not overwritten. Any later defect requires a reviewed release correction under package policy; successful publication is not treated as reversible file replacement.
## Evidence and references
- [Original portfolio PR #1188](https://github.com/ruvnet/RuVector/pull/1188)
- [Frozen source](https://github.com/ruvnet/RuVector/tree/fccf36e9a76d35bce6750dfd3b25802cae3d683b/python/portfolio)
- [Original artifact run](https://github.com/ruvnet/RuVector/actions/runs/38169271248)
- [Security boundaries](../../python/portfolio/SECURITY.md)
- [Frozen file inventory](../../python/portfolio/frozen-release-0.2.0.json)

57
npm/package-lock.json generated
View file

@ -2183,9 +2183,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
"version": "1.32.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz",
"integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9 || ^2.0.5",
@ -5747,14 +5747,14 @@
}
},
"node_modules/axios": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"license": "MIT",
"optional": true,
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"form-data": "^4.0.6",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
@ -6718,14 +6718,15 @@
}
},
"node_modules/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz",
"integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"destroy": "1.2.0",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
@ -6733,6 +6734,10 @@
},
"engines": {
"node": ">= 0.8.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/compression/node_modules/debug": {
@ -8104,9 +8109,9 @@
"license": "MIT"
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.1.0.tgz",
"integrity": "sha512-IuCOCDFM1F/s4LtG7Bjr3yUUwPHybyhqN74xk2cMYoP51vVeErMy8+6HGFchi/+45lW8uzhs6lBeEqks2GWt6A==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
@ -9210,9 +9215,9 @@
}
},
"node_modules/ip-address": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
"integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
"version": "10.7.4",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
"integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==",
"license": "MIT",
"engines": {
"node": ">= 12"
@ -12887,9 +12892,9 @@
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
@ -12897,6 +12902,10 @@
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-from-env": {
@ -12967,9 +12976,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
@ -19720,7 +19729,7 @@
"version": "0.3.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"@ruvector/attention": "^0.1.3",
"@ruvector/core": "^0.1.25",
"@ruvector/gnn": "^0.1.22",
@ -20582,7 +20591,7 @@
"version": "0.1.4",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"@ruvector/rvf": "^0.1.2",
"express": "^4.18.0",
"zod": "^3.22.0"

View file

@ -42,6 +42,7 @@
"tar": "7.5.22",
"tsup": {
"esbuild": "^0.28.1"
}
},
"qs": "^6.16.0"
}
}

View file

@ -50,7 +50,7 @@
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0"
"@modelcontextprotocol/sdk": "^1.32.1"
},
"devDependencies": {
"@types/node": "^20.10.0",

View file

@ -71,7 +71,7 @@
"directory": "npm/packages/ruvector"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"@ruvector/attention": "^0.1.3",
"@ruvector/core": "^0.1.25",
"@ruvector/gnn": "^0.1.22",

View file

@ -33,7 +33,7 @@
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"@ruvector/rvf": "^0.3.4",
"express": "^4.18.0",
"zod": "^3.22.0"

View file

@ -0,0 +1,55 @@
"""Project the changed packages' exact lock closure for a scripts-disabled smoke test.
Usage: python3 npm/tests/prepare-security-consumer.py /new/temporary/directory
Then: cd /new/temporary/directory && npm ci --ignore-scripts && node consumer.cjs
This validates only the affected dependency closure, not the full workspace.
"""
import json
import shutil
import sys
from pathlib import Path
npm = Path(__file__).resolve().parents[1]
packages = json.loads((npm / "package-lock.json").read_text())["packages"]
names = ("@modelcontextprotocol/sdk", "axios", "compression", "fast-copy",
"ip-address", "proxy-addr", "qs")
target = Path(sys.argv[1])
target.mkdir(parents=True, exist_ok=False)
selected = {}
pending = ["node_modules/" + name for name in names]
def resolve(at, name):
parts = at.split("/")
for i in range(len(parts), -1, -1):
if i and parts[i - 1] == "node_modules":
continue
candidate = "/".join(parts[:i] + ["node_modules", name])
if candidate in packages:
return candidate
return None
while pending:
key = pending.pop()
if key in selected:
continue
entry = packages[key]
if entry.get("link"):
raise ValueError("Workspace link outside dependency test scope")
selected[key] = entry
for group in ("dependencies", "optionalDependencies", "peerDependencies"):
for name in entry.get(group, {}):
resolved = resolve(key, name)
if resolved:
pending.append(resolved)
elif group == "dependencies":
raise ValueError(f"Missing required dependency: {key} -> {name}")
dependencies = {name: packages["node_modules/" + name]["version"] for name in names}
manifest = {"name": "ruvector-security-consumer", "version": "1.0.0",
"private": True, "dependencies": dependencies,
"overrides": {"qs": dependencies["qs"]}}
lock = {"name": manifest["name"], "version": "1.0.0", "lockfileVersion": 3,
"requires": True, "packages": {"": manifest, **dict(sorted(selected.items()))}}
(target / "package.json").write_text(json.dumps(manifest, indent=2) + "\n")
(target / "package-lock.json").write_text(json.dumps(lock, indent=2) + "\n")
shutil.copyfile(npm / "tests/security-dependency-consumer.cjs", target / "consumer.cjs")
print(f"Projected {len(selected)} exact dependency records; install scripts remain disabled.")

View file

@ -0,0 +1,49 @@
const assert = require('node:assert/strict');
const http = require('node:http');
const zlib = require('node:zlib');
const axios = require('axios');
const compression = require('compression');
const qs = require('qs');
const proxyaddr = require('proxy-addr');
const {Address4, Address6} = require('ip-address');
const copy = require('fast-copy').default;
const {McpServer} = require('@modelcontextprotocol/sdk/server/mcp.js');
const {Client} = require('@modelcontextprotocol/sdk/client/index.js');
const {InMemoryTransport} = require('@modelcontextprotocol/sdk/inMemory.js');
const {z} = require('zod');
(async () => {
assert.deepEqual(qs.parse('a[b]=1&__proto__[polluted]=yes'), {a:{b:'1'}});
assert.equal({}.polluted, undefined);
const source = {nested:{v:3}, array:[1,2]};
const cloned = copy(source); cloned.nested.v=4;
assert.equal(source.nested.v,3); assert.notEqual(cloned.array,source.array);
assert.equal(new Address4('127.0.0.1').correctForm(),'127.0.0.1');
assert.equal(new Address6('::1').correctForm(),'::1');
const trusted=proxyaddr.compile('loopback');
assert.equal(trusted('127.0.0.1'),true); assert.equal(trusted('8.8.8.8'),false);
const middleware=compression({threshold:0});
const payload='RuVector compression and axios contract '.repeat(200);
const server=http.createServer((req,res)=>middleware(req,res,()=>{res.setHeader('content-type','text/plain');res.end(payload);}));
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
try {
const url='http://127.0.0.1:'+server.address().port;
const result=await axios.get(url,{proxy:false,timeout:3000});
assert.equal(result.data,payload);
const raw=await axios.get(url,{proxy:false,timeout:3000,decompress:false,responseType:'arraybuffer',headers:{'accept-encoding':'gzip'}});
assert.equal(raw.headers['content-encoding'],'gzip');
assert.equal(zlib.gunzipSync(raw.data).toString(),payload);
} finally { await new Promise(resolve=>server.close(resolve)); }
const sdkServer=new McpServer({name:'ruvector-contract',version:'1.0.0'});
sdkServer.registerTool('echo',{inputSchema:{text:z.string().max(32)}},async({text})=>({content:[{type:'text',text}]}));
const sdkClient=new Client({name:'contract-client',version:'1.0.0'});
const [a,b]=InMemoryTransport.createLinkedPair();
try {
await sdkServer.connect(a); await sdkClient.connect(b);
const tools=await sdkClient.listTools(); assert.equal(tools.tools[0].name,'echo');
const answer=await sdkClient.callTool({name:'echo',arguments:{text:'native-safe'}});
assert.equal(answer.content[0].text,'native-safe');
const invalid=await sdkClient.callTool({name:'echo',arguments:{text:'x'.repeat(33)}});
assert.equal(invalid.isError,true);
} finally {await sdkClient.close(); await sdkServer.close();}
console.log('PASS: 7 dependency consumers; MCP tool roundtrip and validation, HTTP compression/axios, query pollution, copy isolation, IP/proxy trust');
})().catch(error=>{console.error(error);process.exitCode=1;});

View file

@ -4,6 +4,19 @@
Python access to the ten most downloaded RuVector npm packages. Eight new companion distributions join the existing `ruvector` SDK and `ruvector-graph`. Previously released clustering and reranking remain available. Every companion has its own introduction, animated diagram, runnable getting started example and explicit capability limits.
## Release status, October 12, 2026
The 0.2.0 portfolio is **partially published**. Attention and cluster wheels are
available; both source archives and nine target releases are still missing.
The original files pin native `ruvector==0.1.3`. Current source targets 0.1.4
and must not replace immutable 0.2.0 artifacts. See the
[verified inventory and recovery evidence](../../docs/python-portfolio/evidence/2026-10-12/README.md)
and [recovery design](../../docs/python-portfolio/resume-release-adr.md).
The published attention package has a
[tested synthetic-data tutorial](tutorials/README.md). Other implemented tracks
below describe source capabilities and do not establish PyPI availability.
## Which packages are most popular?
Measured npm downloads for September 11 through October 10, 2026. These are package retrievals, not unique users. Platform binary variants are excluded. RVF WASM remains a separate distribution because it provides a distinct portable execution backend. [Raw evidence and all source URLs](npm-popularity-2026-10-11.json).
@ -26,7 +39,8 @@ Measured npm downloads for September 11 through October 10, 2026. These are pack
```sh
python -m venv .venv
# Activate the environment using your platform's normal command.
python -m pip install ruvector-core==0.2.0 ruvector-router==0.2.0
python -m pip install ruvector-attention==0.2.0 ruvector==0.1.3 numpy==2.2.6
# Run the published-package example in tutorials/attention_quickstart.py.
```
Choose a task and open its package guide:

View file

@ -0,0 +1,78 @@
# Python portfolio recovery evidence — 2026-10-13
Owner: Codex /root. PR: https://github.com/ruvnet/RuVector/pull/1205.
Repair the existing interrupted 0.2.0 release before selecting a new package.
## Immutable inputs
Source: fccf36e9a76d35bce6750dfd3b25802cae3d683b.
Run: 38169271248; artifact: 11713787141.
Artifact SHA256: b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241.
Inventory SHA256: ca4c4b47df73a26619c048ed1eb02013bfd96484aeb88e20fe16464ef1e0a32d.
At 11:25 UTC only attention and cluster 0.2.0 wheels matched the 22-file
inventory on PyPI; 20 files were missing. No upload was attempted during these
repairs. The native dependency remains exactly ruvector 0.1.3.
This checkpoint does not claim publication.
## Repair evidence
- Run https://github.com/ruvnet/RuVector/actions/runs/38390477781:
62 recovery contracts, frozen native audit, Linux and macOS installed-artifact
acceptance passed. Windows stopped on inventory hash mismatch.
- A scoped LF checkout rule preserves the byte-pinned inventory. A clean Git
index checkout with core.autocrlf=true retained its exact SHA256. The strict
hash check remains unchanged.
- PRs now run all three preparation platforms and the frozen audit. Publication
still requires explicit main dispatch, passing prerequisites and protected
pypi environment.
- Seven targeted npm upgrades clear npm audit. Seven functional consumer groups
pass against a clean 144-record affected dependency closure. Reproduce with
npm/tests/prepare-security-consumer.py and security-dependency-consumer.cjs.
Full workspace installation is not claimed: unrelated lock drift remains.
- Quinn 0.11.18 replaces 0.11.15 for five RustSec advisories with only required
RNG changes. The original SDK all-target normal/build closure contains
neither Quinn nor Reqwest. Local Cargo 1.90 hit an internal resolver panic
before compilation; hosted current-stable checks remain required.
- Replication releases its DashMap guard before accessing another entry.
All 27 tests pass; the new same-shard regression fails within five seconds
against the original implementation.
- Server readiness follows socket binding. The strict test retains its child
stdout pipe through requests. All six tests pass; the affected HTTP case
passed 50 fresh-process runs.
- Only core-and-rest CI moves to Ubuntu 24.04 for ORT ABI requirements.
Hosted whole-shard validation remains required.
An independent review agent approved these code, dependency, checkout and
workflow changes. No assertions or release protections were weakened.
Additional failures must be repaired and reviewed before promotion.
## Additional gate repair
Head fbf30da67aa3da87db6c8c34a6124f5a499d904d passed Windows
recovery preflight and both hosted Cargo vulnerability audits.
The remaining cargo-deny failure identified unmaintained unsafe-libyaml.
Only rvagent-middleware used serde_yaml directly. It now uses pinned
serde-saphyr 1.3.0 (MIT OR Apache-2.0) with only deserialization enabled.
The crate explicitly declares Rust 1.89; the frozen Python SDK is unaffected.
A private visitor retains string-only metadata without numeric-key coercion.
Existing 4KiB frontmatter bounds are supplemented by depth, alias, node,
event and scalar expansion limits. The exact-source parser harness passed
18 existing and five new compatibility/adversarial tests; this excludes
three middleware integration tests, so full hosted crate CI remains required.
An independent review approved the parser, feature restrictions and MSRV
declaration. No advisory exception was added.
## Promotion and rollback
Do not merge or publish with failed or pending required checks. Reconcile PyPI
under the publication lock; upload only byte-identical missing originals,
stop on any failure and never blindly retry an uncertain upload. Then verify
all 22 hashes and fresh PyPI-only installation on Linux, macOS and Windows.
Existing tutorial: https://gist.github.com/ruvnet/2491c7631bf3e186e815196f52fd8954.
Code rollback is a reviewed revert of PR1205. PyPI files cannot be overwritten.
On post-release failure stop promotion, assess yanking with repository owners,
and release a separately reviewed patch version. Never replace frozen 0.2.0
files with rebuilt artifacts.

View file

@ -0,0 +1,120 @@
{
"version": "0.2.0",
"runtime": "0.1.3",
"source_sha": "fccf36e9a76d35bce6750dfd3b25802cae3d683b",
"run_id": 38169271248,
"artifact_id": 11713787141,
"archive_sha256": "b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241",
"files": [
{
"filename": "ruvector_attention-0.2.0-py3-none-any.whl",
"sha256": "48c6557e4fc53c41aeb24b832e3e167afe88fde6c17affbb31cd6da148972bdc",
"bytes": 3629
},
{
"filename": "ruvector_attention-0.2.0.tar.gz",
"sha256": "767881ffe1e0023ed2866ccbb57fdab1c985149ec89d1c003e2e737d7327aa70",
"bytes": 2865
},
{
"filename": "ruvector_cluster-0.2.0-py3-none-any.whl",
"sha256": "99efcf69e29c9a11e47599ea37a725496e11fa88b169224885e23efa0d6ec3b8",
"bytes": 3994
},
{
"filename": "ruvector_cluster-0.2.0.tar.gz",
"sha256": "09e0db4fc6f19a6b0e45b7906c5647adb24917dd5a3b2fa59a282a3629333635",
"bytes": 3269
},
{
"filename": "ruvector_core-0.2.0-py3-none-any.whl",
"sha256": "c269a9cd41dbf6b91900e1709f31e1576560b51e70a553adfeb219d108c1cd89",
"bytes": 4099
},
{
"filename": "ruvector_core-0.2.0.tar.gz",
"sha256": "24a62d2a0b08f4a4a5f1470922f04c8727a75ea36fb4aea3691d087f0d3c5630",
"bytes": 3384
},
{
"filename": "ruvector_gnn-0.2.0-py3-none-any.whl",
"sha256": "4e5e013e3ba87205d2acaabaf1420ada8a37cb9bb8281168395c741c5c6eedcb",
"bytes": 4094
},
{
"filename": "ruvector_gnn-0.2.0.tar.gz",
"sha256": "46706f979d8d45a9d0eb19005f595aa2fe48d88804d04e99acbe22f139a6d100",
"bytes": 3366
},
{
"filename": "ruvector_graph-0.2.0-py3-none-any.whl",
"sha256": "2278acfa6255df9bd961c80ebb36fa28621975b36eb7fb41d992b7b5a921b8cc",
"bytes": 4028
},
{
"filename": "ruvector_graph-0.2.0.tar.gz",
"sha256": "5410b43357250b3719f8c7f700b4cad9e99242bc53532141085ebfb08565ccde",
"bytes": 3319
},
{
"filename": "ruvector_rerank-0.2.0-py3-none-any.whl",
"sha256": "5990bbcc2fabab087480740770f240f10bf65c2bf6400aba6911c27079a9cea4",
"bytes": 4063
},
{
"filename": "ruvector_rerank-0.2.0.tar.gz",
"sha256": "839921d2d7d14387cc572bb210cc356c23b84d2946706dc7432e87afb1bcf87c",
"bytes": 3344
},
{
"filename": "ruvector_router-0.2.0-py3-none-any.whl",
"sha256": "7066ec5b35e3cfb1f5dbb902b87bed18aa85ad10b76f085137e823b5ac0e4938",
"bytes": 4368
},
{
"filename": "ruvector_router-0.2.0.tar.gz",
"sha256": "10c79f3a0f7c86820e7128b9a1e3d426b66f22f248a7eb2a1c92ba891a02eac5",
"bytes": 3579
},
{
"filename": "ruvector_ruvllm-0.2.0-py3-none-any.whl",
"sha256": "ccad8d79a43811148908a97e161f2eebcadbe1ab8975aaa266a0824c3d3cc720",
"bytes": 5085
},
{
"filename": "ruvector_ruvllm-0.2.0.tar.gz",
"sha256": "860a882d5403789c9da73f51e9177a79139eb66617627cb454982da39e4fed4a",
"bytes": 4270
},
{
"filename": "ruvector_rvf-0.2.0-py3-none-any.whl",
"sha256": "7003a7866227bc4ad336c77f677456752c4bc0f6acf37843c6effd94749d496e",
"bytes": 4156
},
{
"filename": "ruvector_rvf-0.2.0.tar.gz",
"sha256": "4d2902065f1212ffbc4861bc316dbc9f7b928397e0840682e8d8ef246527624f",
"bytes": 3520
},
{
"filename": "ruvector_rvf_wasm-0.2.0-py3-none-any.whl",
"sha256": "17bb6bbdb86595c9644a2cac6cb2e631fa2341eb9f87235f2105324aef989e57",
"bytes": 24238
},
{
"filename": "ruvector_rvf_wasm-0.2.0.tar.gz",
"sha256": "566d5d46365188e1493cdace52020f5d9da38232b79580a6b946e549d8cd8028",
"bytes": 23623
},
{
"filename": "ruvector_sona-0.2.0-py3-none-any.whl",
"sha256": "bbf005d0438458a6d1dfdd78e9553eee9cda7933af059450ca7202188eafa4ce",
"bytes": 3675
},
{
"filename": "ruvector_sona-0.2.0.tar.gz",
"sha256": "23db0420e9b1eff2d8c01a61eb84dd1f97e46787a9b0a0ac73cf3908458cd273",
"bytes": 2980
}
]
}

View file

@ -0,0 +1,445 @@
#!/usr/bin/env python3
"""Select missing ORIGINAL 0.2.0 artifacts only after a read-only PyPI precheck.
No rebuilding, uploading, credentials, or skip-existing behavior. All artifacts
must match the frozen successful CI inventory, including already published files.
Runs in an owned CI workspace. POSIX write-mode checks do not model Windows ACLs;
Windows relies on the runner workspace ACL boundary. This is not a sandbox against
concurrent malicious processes or untrusted Windows junctions on older Python.
"""
from __future__ import annotations
import argparse
import email
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import stat
import tarfile
import urllib.error
import urllib.request
import zipfile
VERSION = "0.2.0"
RUNTIME = "0.1.3"
SOURCE_SHA = "fccf36e9a76d35bce6750dfd3b25802cae3d683b"
SOURCE_RUN = 38169271248
SOURCE_ARTIFACT = 11713787141
ARCHIVE_SHA256 = "b4c92cae86c753305abef7aad395fdd9d91c7e84c3c887342defa7a3e4607241"
PACKAGES = tuple(
"ruvector-" + name
for name in (
"attention",
"cluster",
"core",
"gnn",
"graph",
"rerank",
"router",
"ruvllm",
"rvf",
"rvf-wasm",
"sona",
)
)
MAX_FILE_BYTES = 8 * 1024 * 1024
MAX_TOTAL_BYTES = 64 * 1024 * 1024
MAX_MEMBERS = 256
MAX_EXPANDED_BYTES = 16 * 1024 * 1024
MAX_METADATA_BYTES = 256 * 1024
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
# Filled from the immutable CI artifact, never from the live working tree.
INVENTORY_SHA256 = "ca4c4b47df73a26619c048ed1eb02013bfd96484aeb88e20fe16464ef1e0a32d"
DEFAULT_INVENTORY = Path(__file__).with_name("frozen-release-0.2.0.json")
class GateError(ValueError):
"""Sanitized permanent validation failure."""
class RegistryError(RuntimeError):
"""Unavailable registry; retry only after fresh read-only inspection."""
def expected_files() -> dict[str, str]:
return {
filename: package
for package in PACKAGES
for filename in (
f"{package.replace('-', '_')}-{VERSION}-py3-none-any.whl",
f"{package.replace('-', '_')}-{VERSION}.tar.gz",
)
}
def canonical(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
def safe_path(path: Path) -> None:
"""Refuse symlink components; callers must own their working directories."""
absolute = path.absolute()
for component in (absolute, *absolute.parents):
if component.is_symlink():
raise GateError("symlink path component")
def read_regular(path: Path) -> bytes:
safe_path(path)
try:
preliminary = path.lstat()
except OSError as exc:
raise GateError("artifact unavailable") from exc
if not stat.S_ISREG(preliminary.st_mode):
raise GateError("artifact must be a regular file")
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
try:
fd = os.open(path, flags)
except OSError as exc:
raise GateError("artifact cannot be opened safely") from exc
with os.fdopen(fd, "rb") as stream:
info = os.fstat(stream.fileno())
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or info.st_size <= 0
or info.st_size > MAX_FILE_BYTES
):
raise GateError("artifact type, links, or size exceeds bounds")
data = stream.read(MAX_FILE_BYTES + 1)
if len(data) != info.st_size or len(data) > MAX_FILE_BYTES:
raise GateError("artifact changed or exceeds bounds")
return data
def member_path(name: str, prefix: str, *, gitignore: bool = False) -> None:
if not isinstance(name, str) or "\\" in name or "\x00" in name:
raise GateError("unsafe archive path")
parts = PurePosixPath(name).parts
if not name.startswith(prefix) or name.startswith("/") or ".." in parts:
raise GateError("unexpected archive path")
if any(
part in {"__pycache__", "node_modules", "target"}
or (part.startswith(".") and not (gitignore and name == prefix + ".gitignore"))
for part in parts
):
raise GateError("forbidden archive content")
if PurePosixPath(name).suffix in {".pem", ".key", ".so", ".pyd"}:
raise GateError("forbidden archive content")
def metadata(data: bytes, package: str) -> None:
if len(data) > MAX_METADATA_BYTES:
raise GateError("metadata exceeds bounds")
parsed = email.message_from_bytes(data)
if parsed.get_all("Name") != [package] or parsed.get_all("Version") != [VERSION]:
raise GateError("distribution metadata identity mismatch")
runtime_requirements = [
value
for value in parsed.get_all("Requires-Dist", [])
if re.match(r"^ruvector(?:\s|\(|[<>=!~;\[]|$)", value, re.I)
]
if len(runtime_requirements) != 1 or not re.fullmatch(
r"ruvector\s*(?:\(\s*==\s*0\.1\.3\s*\)|==\s*0\.1\.3)\s*",
runtime_requirements[0],
re.I,
):
raise GateError("distribution runtime must be exactly ruvector==0.1.3")
def validate_archive(path: Path, package: str) -> None:
normalized = package.replace("-", "_")
if path.name.endswith(".whl"):
with zipfile.ZipFile(path) as archive:
members = archive.infolist()
if len(members) > MAX_MEMBERS or len({m.filename for m in members}) != len(
members
):
raise GateError("archive has too many or duplicate members")
if sum(m.file_size for m in members) > MAX_EXPANDED_BYTES:
raise GateError("archive expanded size exceeds bounds")
found = []
for member in members:
mode = member.external_attr >> 16
if stat.S_ISLNK(mode) or member.flag_bits & 1:
raise GateError("wheel symlink or encrypted content")
prefix = normalized + "/"
if member.filename.startswith(f"{normalized}-{VERSION}.dist-info/"):
prefix = f"{normalized}-{VERSION}.dist-info/"
member_path(member.filename, prefix)
if member.filename.endswith(".dist-info/METADATA"):
if member.file_size > MAX_METADATA_BYTES:
raise GateError("metadata exceeds bounds")
found.append(archive.read(member))
if len(found) != 1:
raise GateError("missing or ambiguous wheel metadata")
metadata(found[0], package)
else:
with tarfile.open(path, mode="r:gz") as archive:
count, expanded, found = 0, 0, []
prefix = f"{normalized}-{VERSION}/"
allowed = {
"src",
"README.md",
"LICENSE",
"pyproject.toml",
"PKG-INFO",
".gitignore",
}
seen = set()
for member in archive:
count += 1
expanded += member.size
if count > MAX_MEMBERS or expanded > MAX_EXPANDED_BYTES:
raise GateError("sdist allocation exceeds bounds")
if member.name in seen or not (member.isfile() or member.isdir()):
raise GateError("duplicate or unsafe sdist member")
seen.add(member.name)
member_path(member.name, prefix, gitignore=True)
if member.name[len(prefix) :].split("/")[0] not in allowed:
raise GateError("unexpected sdist content")
if member.name == prefix + "PKG-INFO":
if member.size > MAX_METADATA_BYTES:
raise GateError("metadata exceeds bounds")
stream = archive.extractfile(member)
if stream is None:
raise GateError("missing sdist metadata")
found.append(stream.read(MAX_METADATA_BYTES + 1))
if len(found) != 1:
raise GateError("missing or ambiguous sdist metadata")
metadata(found[0], package)
def fetch_json(package: str, version: str) -> dict | None:
request = urllib.request.Request(
f"https://pypi.org/pypi/{package}/{version}/json",
headers={"User-Agent": "RuVector-frozen-release-precheck/1"},
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
if response.status != 200:
raise RegistryError("unexpected registry response")
data = response.read(MAX_RESPONSE_BYTES + 1)
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise RegistryError(f"registry HTTP {exc.code}; no files selected") from exc
except OSError as exc:
raise RegistryError("registry unavailable; no files selected") from exc
if len(data) > MAX_RESPONSE_BYTES:
raise RegistryError("registry response exceeds bounds")
try:
result = json.loads(data)
except (ValueError, UnicodeError) as exc:
raise RegistryError("malformed registry response") from exc
if not isinstance(result, dict):
raise RegistryError("invalid registry response")
return result
def registry_files(data: dict | None, package: str, version: str) -> list[dict]:
if data is None:
return []
if not isinstance(data, dict):
raise GateError("invalid registry metadata")
info, urls = data.get("info"), data.get("urls")
if (
not isinstance(info, dict)
or not isinstance(info.get("name"), str)
or canonical(info["name"]) != package
or info.get("version") != version
or not isinstance(urls, list)
or not urls
or len(urls) > 128
):
raise GateError("registry identity, version, or file inventory mismatch")
seen = set()
for item in urls:
if (
not isinstance(item, dict)
or not isinstance(item.get("filename"), str)
or item["filename"] in seen
or item.get("yanked") is not False
or not isinstance(item.get("digests"), dict)
or not isinstance(item["digests"].get("sha256"), str)
or not re.fullmatch(r"[0-9a-f]{64}", item["digests"]["sha256"])
):
raise GateError("registry duplicate, yanked, or invalid file")
seen.add(item["filename"])
return urls
def load_inventory(path: Path) -> dict[str, str]:
data = read_regular(path)
if hashlib.sha256(data).hexdigest() != INVENTORY_SHA256:
raise GateError("frozen inventory hash mismatch")
try:
inventory = json.loads(data)
except ValueError as exc:
raise GateError("invalid frozen inventory") from exc
required = {
"version": VERSION,
"runtime": RUNTIME,
"source_sha": SOURCE_SHA,
"run_id": SOURCE_RUN,
"artifact_id": SOURCE_ARTIFACT,
"archive_sha256": ARCHIVE_SHA256,
}
if not isinstance(inventory, dict) or any(
inventory.get(k) != v for k, v in required.items()
):
raise GateError("frozen inventory source identity mismatch")
files = inventory.get("files")
if not isinstance(files, list) or len(files) != 22:
raise GateError("frozen inventory file count mismatch")
result = {}
for item in files:
if (
not isinstance(item, dict)
or item.get("filename") not in expected_files()
or item["filename"] in result
or not isinstance(item.get("sha256"), str)
or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"])
or type(item.get("bytes")) is not int
or not 0 < item["bytes"] <= MAX_FILE_BYTES
):
raise GateError("invalid frozen inventory entry")
result[item["filename"]] = item["sha256"]
if (
set(result) != set(expected_files())
or sum(item["bytes"] for item in files) > MAX_TOTAL_BYTES
):
raise GateError("frozen inventory file set or size mismatch")
return result
def directory_mode_safe(mode: int, platform: str = os.name) -> bool:
"""POSIX mode checks; Windows trusts the CI workspace ACL boundary."""
return platform == "nt" or not (mode & 0o022)
def precheck(
directory: Path, *, inventory=DEFAULT_INVENTORY, fetcher=fetch_json
) -> tuple[dict, dict[str, bytes]]:
safe_path(directory)
if not directory.is_dir() or not directory_mode_safe(directory.stat().st_mode):
raise GateError("artifact directory must be private to its owner")
expected = expected_files()
frozen = load_inventory(inventory)
if {p.name for p in directory.iterdir()} != set(expected):
raise GateError("missing or unexpected release artifacts")
contents = {}
total = 0
for filename, package in sorted(expected.items()):
data = read_regular(directory / filename)
total += len(data)
if total > MAX_TOTAL_BYTES:
raise GateError("total artifact size exceeds bounds")
digest = hashlib.sha256(data).hexdigest()
if digest != frozen[filename]:
raise GateError(f"frozen artifact hash mismatch: {filename}")
try:
validate_archive(directory / filename, package)
except (OSError, zipfile.BadZipFile, tarfile.TarError, RuntimeError) as exc:
raise GateError("invalid distribution archive") from exc
contents[filename] = data
native = registry_files(fetcher("ruvector", RUNTIME), "ruvector", RUNTIME)
if not native:
raise GateError("required native runtime is not published")
published = []
for package in PACKAGES:
files = registry_files(fetcher(package, VERSION), package, VERSION)
package_expected = {
name for name, owner in expected.items() if owner == package
}
for item in files:
filename = item["filename"]
if filename not in package_expected:
raise GateError("unknown registry artifact")
if item["digests"]["sha256"] != frozen[filename]:
raise GateError(f"published artifact hash mismatch: {filename}")
published.append(filename)
missing = sorted(set(expected) - set(published))
plan = {
"schema": 1,
"operation": "precheck-only",
"version": VERSION,
"runtime": RUNTIME,
"source_sha": SOURCE_SHA,
"source_run": SOURCE_RUN,
"source_artifact": SOURCE_ARTIFACT,
"archive_sha256": ARCHIVE_SHA256,
"published": sorted(published),
"missing": missing,
"files": {name: frozen[name] for name in sorted(expected)},
"promotion": "independent-review-and-protected-environment-required",
}
return plan, {name: contents[name] for name in missing}
def stage(
directory: Path, output: Path, *, inventory=DEFAULT_INVENTORY, fetcher=fetch_json
) -> dict:
"""Write a new directory only after every local and registry check passes."""
safe_path(output)
if output.exists():
raise GateError("staging destination already exists")
plan, files = precheck(directory, inventory=inventory, fetcher=fetcher)
output.mkdir(mode=0o700)
try:
for name, data in files.items():
with (output / name).open("xb") as stream:
stream.write(data)
except BaseException:
shutil.rmtree(output)
raise
return plan
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dist", required=True, type=Path)
parser.add_argument("--inventory", type=Path, default=DEFAULT_INVENTORY)
parser.add_argument(
"--missing-dir", type=Path, help="new directory for missing distributions only"
)
parser.add_argument("--receipt", type=Path, help="new JSON receipt file")
parser.add_argument(
"--verify-only",
action="store_true",
help="require every original file published",
)
args = parser.parse_args()
try:
if args.verify_only and args.missing_dir:
raise GateError("verify-only cannot select files")
if args.receipt:
safe_path(args.receipt)
if args.receipt.exists():
raise GateError("receipt destination already exists")
result = (
stage(args.dist, args.missing_dir, inventory=args.inventory)
if args.missing_dir
else precheck(args.dist, inventory=args.inventory)[0]
)
if args.verify_only and result["missing"]:
raise GateError("original distributions remain unpublished")
if args.receipt:
with args.receipt.open("x", encoding="utf-8") as stream:
json.dump(
{"status": "validated", **result}, stream, indent=2, sort_keys=True
)
stream.write("\n")
except (GateError, RegistryError) as exc:
print(json.dumps({"status": "blocked", "reason": str(exc)}))
return 1
print(json.dumps({"status": "validated", **result}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,486 @@
import copy
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path
import tarfile
import urllib.error
import zipfile
import pytest
SPEC = importlib.util.spec_from_file_location(
"resume", Path(__file__).parents[1] / "resume_release.py"
)
resume = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(resume)
def digest(data):
return hashlib.sha256(data).hexdigest()
def distribution_metadata(package, runtime="0.1.3"):
return f"Metadata-Version: 2.4\nName: {package}\nVersion: 0.2.0\nRequires-Dist: ruvector=={runtime}\n\n".encode()
def write_wheel(path, package, extra=None, runtime="0.1.3"):
normalized = package.replace("-", "_")
with zipfile.ZipFile(path, "w") as archive:
archive.writestr(
f"{normalized}-0.2.0.dist-info/METADATA",
distribution_metadata(package, runtime),
)
archive.writestr(f"{normalized}/__init__.py", "# synthetic fixture\n")
if extra:
archive.writestr(extra, "bad")
def write_sdist(path, package, extra=None, runtime="0.1.3"):
normalized = package.replace("-", "_")
with tarfile.open(path, "w:gz") as archive:
member = tarfile.TarInfo(f"{normalized}-0.2.0/PKG-INFO")
data = distribution_metadata(package, runtime)
member.size = len(data)
archive.addfile(member, io.BytesIO(data))
if extra:
archive.addfile(extra)
@pytest.fixture
def release(tmp_path, monkeypatch):
directory = tmp_path / "dist"
directory.mkdir()
inventory = tmp_path / "inventory.json"
for filename, package in resume.expected_files().items():
path = directory / filename
(write_wheel if filename.endswith(".whl") else write_sdist)(path, package)
def freeze():
data = {
"version": resume.VERSION,
"runtime": resume.RUNTIME,
"source_sha": resume.SOURCE_SHA,
"run_id": resume.SOURCE_RUN,
"artifact_id": resume.SOURCE_ARTIFACT,
"archive_sha256": resume.ARCHIVE_SHA256,
"files": [
{
"filename": p.name,
"bytes": p.stat().st_size,
"sha256": digest(p.read_bytes()),
}
for p in sorted(directory.iterdir())
if p.is_file()
],
}
inventory.write_text(json.dumps(data))
monkeypatch.setattr(resume, "INVENTORY_SHA256", digest(inventory.read_bytes()))
return data
frozen = freeze()
native = {
"info": {"name": "ruvector", "version": "0.1.3"},
"urls": [
{"filename": "native.whl", "yanked": False, "digests": {"sha256": "a" * 64}}
],
}
registry = {"ruvector": native}
for package in resume.PACKAGES:
registry[package] = {
"info": {"name": package, "version": "0.2.0"},
"urls": [
{
"filename": f["filename"],
"yanked": False,
"digests": {"sha256": f["sha256"]},
}
for f in frozen["files"]
if resume.expected_files()[f["filename"]] == package
],
}
def fetcher(package, version):
return registry.get(package)
return directory, inventory, registry, fetcher, freeze
def test_complete_release_selects_nothing(release, tmp_path):
directory, inventory, _, fetcher, _ = release
output = tmp_path / "missing"
plan = resume.stage(directory, output, inventory=inventory, fetcher=fetcher)
assert not plan["missing"] and len(plan["published"]) == 22
assert list(output.iterdir()) == []
def test_partial_release_selects_only_absent_files(release, tmp_path):
directory, inventory, registry, fetcher, _ = release
first = resume.PACKAGES[0]
missing = registry[first]["urls"].pop()["filename"]
del registry[resume.PACKAGES[1]]
output = tmp_path / "missing"
plan = resume.stage(directory, output, inventory=inventory, fetcher=fetcher)
assert len(plan["missing"]) == 3 and missing in plan["missing"]
assert {p.name for p in output.iterdir()} == set(plan["missing"])
assert all(
digest(p.read_bytes()) == plan["files"][p.name] for p in output.iterdir()
)
@pytest.mark.parametrize(
"fault",
[
"hash",
"unknown",
"yanked",
"yanked_missing",
"wrong_version",
"wrong_name",
"duplicate",
"digest_missing",
"upper_digest",
"empty_urls",
"wrong_urls_type",
],
)
def test_registry_faults_write_no_selection(release, tmp_path, fault):
directory, inventory, registry, fetcher, _ = release
data = registry[resume.PACKAGES[-1]]
if fault == "hash":
data["urls"][0]["digests"]["sha256"] = "a" * 64
elif fault == "unknown":
data["urls"][0]["filename"] = "unknown.whl"
elif fault == "yanked":
data["urls"][0]["yanked"] = True
elif fault == "yanked_missing":
data["urls"][0].pop("yanked")
elif fault in {"wrong_version", "wrong_name"}:
data["info"]["version" if fault == "wrong_version" else "name"] = "unexpected"
elif fault == "duplicate":
data["urls"].append(copy.deepcopy(data["urls"][0]))
elif fault == "digest_missing":
data["urls"][0]["digests"] = {}
elif fault == "upper_digest":
data["urls"][0]["digests"]["sha256"] = "A" * 64
elif fault == "empty_urls":
data["urls"] = []
else:
data["urls"] = {}
output = tmp_path / "missing"
with pytest.raises(resume.GateError):
resume.stage(directory, output, inventory=inventory, fetcher=fetcher)
assert not output.exists()
def test_late_registry_outage_writes_nothing(release, tmp_path):
directory, inventory, registry, fetcher, _ = release
del registry[resume.PACKAGES[0]]
def outage(package, version):
if package == resume.PACKAGES[-1]:
raise resume.RegistryError("registry HTTP 429")
return fetcher(package, version)
output = tmp_path / "missing"
with pytest.raises(resume.RegistryError):
resume.stage(directory, output, inventory=inventory, fetcher=outage)
assert not output.exists()
def test_native_runtime_required(release):
directory, inventory, registry, fetcher, _ = release
del registry["ruvector"]
with pytest.raises(resume.GateError, match="native"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
@pytest.mark.parametrize(
"fault", ["extra", "absent", "hash", "symlink", "hardlink", "fifo", "oversize"]
)
def test_local_faults_fail_before_registry(release, tmp_path, fault, monkeypatch):
directory, inventory, _, _, _ = release
if fault == "fifo" and not hasattr(os, "mkfifo"):
pytest.skip("POSIX FIFO files are unavailable on this platform")
path = next(iter(directory.iterdir()))
if fault == "extra":
(directory / "unexpected.txt").write_text("extra")
elif fault == "absent":
path.unlink()
elif fault == "hash":
path.write_bytes(b"wrong")
elif fault == "symlink":
original = path.read_bytes()
path.unlink()
external = tmp_path / "outside"
external.write_bytes(original)
path.symlink_to(external)
elif fault == "hardlink":
os.link(path, tmp_path / "linked")
elif fault == "fifo":
path.unlink()
os.mkfifo(path)
else:
monkeypatch.setattr(resume, "MAX_FILE_BYTES", 1)
def should_not_fetch(*args):
pytest.fail("invalid local artifacts must not reach registry")
with pytest.raises(resume.GateError):
resume.precheck(directory, inventory=inventory, fetcher=should_not_fetch)
def test_inventory_tampering(release):
directory, inventory, _, fetcher, _ = release
inventory.write_text(inventory.read_text() + " ")
with pytest.raises(resume.GateError, match="inventory hash"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
@pytest.mark.parametrize(
"field,value",
[
("runtime", "0.1.4"),
("version", "0.3.0"),
("source_sha", "a" * 40),
("run_id", 1),
("artifact_id", 1),
("archive_sha256", "b" * 64),
],
)
def test_inventory_identity_bound(release, monkeypatch, field, value):
directory, inventory, _, fetcher, _ = release
content = json.loads(inventory.read_bytes())
content[field] = value
inventory.write_text(json.dumps(content))
monkeypatch.setattr(resume, "INVENTORY_SHA256", digest(inventory.read_bytes()))
with pytest.raises(resume.GateError, match="source identity"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
@pytest.mark.parametrize("suffix", [".whl", ".tar.gz"])
def test_wrong_runtime_rejected_even_when_fixture_hash_updated(release, suffix):
directory, inventory, _, fetcher, freeze = release
filename, package = next(
(n, p) for n, p in resume.expected_files().items() if n.endswith(suffix)
)
path = directory / filename
(write_wheel if suffix == ".whl" else write_sdist)(path, package, runtime="0.1.4")
freeze()
with pytest.raises(resume.GateError, match="runtime"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
@pytest.mark.parametrize(
"path",
[
"../escape",
"/absolute",
"ruvector_attention/.secret",
"ruvector_attention/key.pem",
],
)
def test_wheel_unsafe_paths(release, path):
directory, inventory, _, fetcher, freeze = release
wheel = directory / "ruvector_attention-0.2.0-py3-none-any.whl"
write_wheel(wheel, "ruvector-attention", extra=path)
freeze()
with pytest.raises(resume.GateError):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
@pytest.mark.parametrize("kind", ["symlink", "hardlink", "device", "fifo", "traversal"])
def test_sdist_unsafe_members(release, kind):
directory, inventory, _, fetcher, freeze = release
member = tarfile.TarInfo("ruvector_attention-0.2.0/src/bad")
member.type = {
"symlink": tarfile.SYMTYPE,
"hardlink": tarfile.LNKTYPE,
"device": tarfile.CHRTYPE,
"fifo": tarfile.FIFOTYPE,
"traversal": tarfile.REGTYPE,
}[kind]
if kind == "traversal":
member.name = "ruvector_attention-0.2.0/../escape"
write_sdist(
directory / "ruvector_attention-0.2.0.tar.gz",
"ruvector-attention",
extra=member,
)
freeze()
with pytest.raises(resume.GateError):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
def test_archive_member_and_expansion_bounds(release, monkeypatch):
directory, inventory, _, fetcher, _ = release
monkeypatch.setattr(resume, "MAX_MEMBERS", 1)
with pytest.raises(resume.GateError, match="members"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
monkeypatch.setattr(resume, "MAX_MEMBERS", 256)
monkeypatch.setattr(resume, "MAX_EXPANDED_BYTES", 1)
with pytest.raises(resume.GateError, match="expanded"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
def test_existing_staging_never_overwritten(release, tmp_path):
directory, inventory, _, fetcher, _ = release
output = tmp_path / "missing"
output.mkdir()
sentinel = output / "keep"
sentinel.write_text("retained")
with pytest.raises(resume.GateError, match="already exists"):
resume.stage(directory, output, inventory=inventory, fetcher=fetcher)
assert sentinel.read_text() == "retained"
@pytest.mark.parametrize("status", [403, 429, 500])
def test_http_errors_are_unavailable_not_absent(monkeypatch, status):
def fail(*args, **kwargs):
raise urllib.error.HTTPError("https://pypi.org", status, "", {}, None)
monkeypatch.setattr(resume.urllib.request, "urlopen", fail)
with pytest.raises(resume.RegistryError, match=str(status)):
resume.fetch_json("ruvector-core", "0.2.0")
def test_http_404_means_absent(monkeypatch):
def absent(*args, **kwargs):
raise urllib.error.HTTPError("https://pypi.org", 404, "", {}, None)
monkeypatch.setattr(resume.urllib.request, "urlopen", absent)
assert resume.fetch_json("ruvector-core", "0.2.0") is None
def test_duplicate_metadata_identity_rejected():
data = distribution_metadata("ruvector-attention").replace(
b"Version:", b"Name: ruvector-attention\nVersion:"
)
with pytest.raises(resume.GateError, match="identity"):
resume.metadata(data, "ruvector-attention")
def test_runtime_environment_marker_and_extra_rejected():
for requirement in ("ruvector==0.1.3; python_version<'3.13'", "ruvector[x]==0.1.3"):
data = distribution_metadata("ruvector-attention").replace(
b"ruvector==0.1.3", requirement.encode()
)
with pytest.raises(resume.GateError, match="runtime"):
resume.metadata(data, "ruvector-attention")
@pytest.mark.parametrize(
"verify,missing,expected", [(True, False, 0), (True, True, 1), (False, True, 0)]
)
def test_cli_verify_receipt(
release, tmp_path, monkeypatch, capsys, verify, missing, expected
):
directory, inventory, registry, fetcher, _ = release
if missing:
del registry[resume.PACKAGES[-1]]
original = resume.precheck
monkeypatch.setattr(
resume,
"precheck",
lambda path, inventory: original(path, inventory=inventory, fetcher=fetcher),
)
receipt = tmp_path / "receipt.json"
argv = [
"resume",
"--dist",
str(directory),
"--inventory",
str(inventory),
"--receipt",
str(receipt),
]
if verify:
argv.append("--verify-only")
monkeypatch.setattr(resume, "__name__", "resume")
monkeypatch.setattr("sys.argv", argv)
assert resume.main() == expected
output = json.loads(capsys.readouterr().out)
assert output["status"] == ("validated" if expected == 0 else "blocked")
assert receipt.exists() == (expected == 0)
if expected == 0:
assert json.loads(receipt.read_text()) == output
def test_cli_verify_cannot_select(tmp_path, monkeypatch, capsys):
output = tmp_path / "missing"
monkeypatch.setattr(
"sys.argv",
[
"resume",
"--dist",
str(tmp_path),
"--missing-dir",
str(output),
"--verify-only",
],
)
assert resume.main() == 1
assert "cannot select" in capsys.readouterr().out
assert not output.exists()
@pytest.mark.parametrize(
"body", [b"[]", b"null", b"not-json", b"x" * (resume.MAX_RESPONSE_BYTES + 1)]
)
def test_registry_response_bounds_and_schema(monkeypatch, body):
class Response:
status = 200
def __enter__(self):
return self
def __exit__(self, *args):
return None
def read(self, limit):
return body[:limit]
monkeypatch.setattr(
resume.urllib.request, "urlopen", lambda *args, **kwargs: Response()
)
with pytest.raises(resume.RegistryError):
resume.fetch_json("ruvector-core", "0.2.0")
def test_metadata_allocation_bound(monkeypatch):
monkeypatch.setattr(resume, "MAX_METADATA_BYTES", 5)
with pytest.raises(resume.GateError, match="exceeds bounds"):
resume.metadata(
distribution_metadata("ruvector-attention"), "ruvector-attention"
)
@pytest.mark.skipif(
os.name == "nt", reason="POSIX chmod does not model Windows workspace ACLs"
)
def test_owned_directory_cannot_be_group_writable(release):
directory, inventory, _, fetcher, _ = release
directory.chmod(0o775)
with pytest.raises(resume.GateError, match="private"):
resume.precheck(directory, inventory=inventory, fetcher=fetcher)
def test_windows_synthetic_mode_0777_uses_trusted_workspace_acl(release, monkeypatch):
directory, inventory, _, fetcher, _ = release
directory.chmod(0o777)
original = resume.directory_mode_safe
assert original(0o777, "nt")
assert not original(0o777, "posix")
monkeypatch.setattr(
resume, "directory_mode_safe", lambda mode: original(mode, "nt")
)
plan, files = resume.precheck(directory, inventory=inventory, fetcher=fetcher)
assert len(plan["published"]) == 22 and not files
@pytest.mark.parametrize("mode", [0o775, 0o757, 0o777])
def test_posix_directory_write_mode_guard(mode):
assert not resume.directory_mode_safe(mode, "posix")
assert resume.directory_mode_safe(0o755, "posix")

View file

@ -0,0 +1,42 @@
# Native attention quickstart
Use published `ruvector-attention==0.2.0` to blend synthetic embeddings with scaled dot product attention. This example calls the RuVector Rust runtime and checks its output against an independent NumPy reference.
## Install and run
Python 3.10 or later; the native `ruvector==0.1.3` wheel must support your platform. Save this README and `attention_quickstart.py` together.
```sh
uv venv .venv
uv pip install --python .venv/bin/python ruvector-attention==0.2.0 ruvector==0.1.3 numpy==2.2.6
.venv/bin/python attention_quickstart.py
```
On Windows use `.venv/Scripts/python.exe` in the two commands above. The exact script is [attention_quickstart.py](attention_quickstart.py).
Expected output:
```text
ruvector-attention=0.2.0 ruvector=0.1.3
weights=0.575975,0.283995,0.140029
blended=0.435946,0.283995
PASS: native attention matches reference; invalid inputs rejected
```
## What the example proves
The three candidates act as both keys and values. The runtime computes `softmax(query @ candidates.T / sqrt(dimension))` and their weighted sum. The first candidate receives the greatest weight. The example also verifies rejection of a dimension mismatch and a nonfinite query.
Fresh PyPI installation and the exact script passed on October 12, 2026, using CPython 3.10.22, Linux x86_64, glibc 2.39 and NumPy 2.2.6. This is a numerical correctness smoke test, with no speed or quality benchmark. This run does not establish fresh macOS or Windows execution.
## Scope and limitations
There is no model download, text generation, trained cross encoder or model training. Attention ordering is dot product ordering. The package exposes this attention operation, not full npm feature parity. The wrapper converts inputs to contiguous float32; conversion can allocate before shape validation. Use external resource limits for untrusted workloads.
The published 0.2.0 wheel requires `ruvector==0.1.3`. Current repository main changes that dependency to 0.1.4. This tutorial deliberately reproduces the immutable published artifact and does not install unreleased source.
## Sources and release recovery
[PyPI package](https://pypi.org/project/ruvector-attention/0.2.0/) · [Native runtime](https://pypi.org/project/ruvector/0.1.3/) · [Immutable source](https://github.com/ruvnet/RuVector/tree/fccf36e9a76d35bce6750dfd3b25802cae3d683b/python/portfolio/ruvector-attention) · [Portfolio PR #1188](https://github.com/ruvnet/RuVector/pull/1188) · [Recovery ADR](../../../docs/python-portfolio/resume-release-adr.md)
The portfolio release is partially delivered. Recovery checks every existing filename and checksum against the original artifact ledger, then permits only missing original files through protected publishing. That does not change the successfully published attention wheel used here.

View file

@ -0,0 +1,48 @@
"""Synthetic scaled dot product attention using published RuVector packages.
Requires ruvector-attention==0.2.0, ruvector==0.1.3 and numpy==2.2.6.
This validates attention math, not model training or text generation.
"""
from __future__ import annotations
from importlib.metadata import version
import numpy as np
from ruvector_attention import attend
def main() -> None:
assert version("ruvector-attention") == "0.2.0"
assert version("ruvector") == "0.1.3"
query = np.array([1.0, 0.0], dtype=np.float32)
candidates = np.array([[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0]], dtype=np.float32)
result = attend(query, candidates)
# Independent float64 reference; subtracting max prevents exp overflow.
logits = candidates.astype(np.float64) @ query.astype(np.float64) / np.sqrt(2.0)
reference = np.exp(logits - logits.max())
reference /= reference.sum()
np.testing.assert_allclose(result.weights, reference, rtol=1e-6, atol=1e-7)
np.testing.assert_allclose(result.blended, reference @ candidates, rtol=1e-6, atol=1e-7)
assert result.blended.shape == (2,)
assert np.isclose(result.weights.sum(), 1.0)
assert int(np.argmax(result.weights)) == 0
rejected = 0
for bad_query in ([1.0], [float("nan"), 0.0]):
try:
attend(bad_query, candidates)
except ValueError:
rejected += 1
else:
raise AssertionError("invalid input was accepted")
assert rejected == 2
print("ruvector-attention=0.2.0 ruvector=0.1.3")
print("weights=" + ",".join(f"{weight:.6f}" for weight in result.weights))
print("blended=" + ",".join(f"{coordinate:.6f}" for coordinate in result.blended))
print("PASS: native attention matches reference; invalid inputs rejected")
if __name__ == "__main__":
main()