Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@
- Added a deterministic synthetic retrieval-poisoning Failure Zoo specimen with explicit unsupported-citation, unsupported-claim, and forbidden-output findings.
- Added checkout-free Windows verification of the published package, least-privilege CodeQL scanning, and protected-branch release gates.
- Added disabled-by-default local usage evidence that records only command, exit code, version, and UTC timestamp without arguments or paths.
- Generate the CycloneDX SBOM from the built wheel in a dependency-free environment so its root version is exact and no local build path is disclosed.

### Tiếng Việt

- Bổ sung mẫu synthetic retrieval poisoning xác định với các finding rõ ràng về citation không được hỗ trợ, claim không có căn cứ và output bị cấm.
- Bổ sung kiểm tra Windows không checkout từ package đã public, CodeQL theo least privilege và release gate bảo vệ nhánh chính.
- Bổ sung bằng chứng sử dụng local mặc định tắt, chỉ ghi command, exit code, version và UTC timestamp, không ghi arguments hoặc paths.
- Tạo CycloneDX SBOM từ wheel đã build trong môi trường không dependency để version gốc chính xác và không lộ local build path.

### 日本語

- unsupported citation、unsupported claim、forbidden output を明示する決定的な synthetic retrieval-poisoning Failure Zoo specimen を追加しました。
- 公開 package の checkout-free Windows 検証、least-privilege CodeQL、保護 branch release gate を追加しました。
- デフォルト無効で、引数や path を記録せず command、exit code、version、UTC timestamp のみを残す local usage evidence を追加しました。
- build 済み wheel を dependency-free environment で読み取り、正確な root version と local build path を含まない CycloneDX SBOM を生成します。

## [2.0.1] - 2026-08-23

Expand Down
1 change: 1 addition & 0 deletions docs/releases/v2.0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ RAGOps 2.0.2 hardens the public 2.0 release path without changing the offline ev
- Verifies the published package on checkout-free Windows and preserves the same synthetic `BLOCK` decision and evidence verification.
- Adds least-privilege CodeQL scanning and protected-main release gates for Python 3.11, 3.12, 3.13, Windows, and CodeQL.
- Adds disabled-by-default local usage evidence containing only UTC timestamp, RAGOps version, top-level command, and exit code. Arguments and paths are not recorded, and no event is sent over the network.
- Builds the CycloneDX SBOM from the release wheel in a dependency-free environment, validating the exact RAGOps root version and rejecting local file URLs.

The credential-free flagship demo still intentionally blocks a synthetic regression: citation coverage `1.0 -> 0.5`, citation precision `1.0 -> 0.5`, and lexical groundedness `1.0 -> 0.6`. This is reproducible synthetic evidence, not production adoption evidence.

Expand Down
2 changes: 1 addition & 1 deletion docs/submission/DIRECTORY_SUBMISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ No account or credential is required. Use Python 3.11+ and run the bundled wrapp

## v2.0.2 update notes

RAGOps 2.0.2 adds a synthetic retrieval-poisoning regression specimen, verified checkout-free Windows use of the published package, CodeQL and protected-branch gates, and explicit opt-in local usage evidence. It retains the 2.0 content-addressed evidence, slice-aware policy, calibration, trace-graph, dataset leakage, CI output, portable import, and blind-review contracts. The plugin remains skills-only and does not include a hosted MCP connector. The optional local API/workbench is distributed through the Python package, not as a remote ChatGPT connector.
RAGOps 2.0.2 adds a synthetic retrieval-poisoning regression specimen, verified checkout-free Windows use of the published package, CodeQL and protected-branch gates, explicit opt-in local usage evidence, and a wheel-derived SBOM with no local build paths. It retains the 2.0 content-addressed evidence, slice-aware policy, calibration, trace-graph, dataset leakage, CI output, portable import, and blind-review contracts. The plugin remains skills-only and does not include a hosted MCP connector. The optional local API/workbench is distributed through the Python package, not as a remote ChatGPT connector.

## Final portal checks

Expand Down
57 changes: 51 additions & 6 deletions scripts/local_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,56 @@ def verify_clean_install(wheel: Path, expected_version: str) -> None:
run(str(python), "-m", "ragops.cli", "evidence", "verify", "--bundle", str(demo / "evidence"))


def prepare_sbom_environment(wheel: Path, venv: Path) -> Path:
run(sys.executable, "-m", "venv", "--without-pip", str(venv))
python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
site_packages = Path(
subprocess.check_output(
[str(python), "-c", "import site; print(site.getsitepackages()[0])"],
text=True,
).strip()
)
with zipfile.ZipFile(wheel) as archive:
archive.extractall(site_packages)
return python


def validate_sbom(sbom: Path, expected_version: str) -> None:
data = json.loads(sbom.read_text(encoding="utf-8"))
root = data.get("metadata", {}).get("component", {})
if root.get("name") != "ragops" or root.get("version") != expected_version:
raise SystemExit("SBOM root component does not match package version")
if data.get("components"):
raise SystemExit("dependency-free RAGOps SBOM must not contain dependency components")
if data.get("dependencies") != [{"ref": root.get("bom-ref")}]:
raise SystemExit("dependency-free RAGOps SBOM has an unexpected dependency graph")
for component in [root, *data.get("components", [])]:
for reference in component.get("externalReferences", []):
if str(reference.get("url", "")).lower().startswith("file:"):
raise SystemExit("SBOM must not contain a local file URL")


def build_sbom(wheel: Path, sbom: Path, expected_version: str) -> None:
cyclonedx = environment_tool("cyclonedx-py")
if not (Path(cyclonedx).is_file() or shutil.which(cyclonedx)):
raise SystemExit("cyclonedx-py is required: python -m pip install cyclonedx-bom==7.3.0")
with tempfile.TemporaryDirectory(prefix="ragops-sbom-") as temp:
python = prepare_sbom_environment(wheel, Path(temp) / "venv")
run(
cyclonedx,
"environment",
str(python),
"--pyproject",
str(ROOT / "pyproject.toml"),
"--mc-type",
"library",
"--output-reproducible",
"--output-file",
str(sbom),
)
validate_sbom(sbom, expected_version)


def build_plugin_bundle(expected_version: str) -> Path:
manifest = json.loads((ROOT / ".codex-plugin/plugin.json").read_text(encoding="utf-8"))
if manifest.get("version") != expected_version:
Expand Down Expand Up @@ -186,12 +236,7 @@ def verify(tag: str) -> None:
verify_clean_install(wheel, version())
plugin = build_plugin_bundle(version())
sbom = DIST / f"ragops-{version()}.cdx.json"
cyclonedx = environment_tool("cyclonedx-py")
if Path(cyclonedx).is_file() or shutil.which(cyclonedx):
run(cyclonedx, "environment", sys.executable, "--output-reproducible",
"--output-file", str(sbom))
else:
raise SystemExit("cyclonedx-py is required: python -m pip install cyclonedx-bom==7.3.0")
build_sbom(wheel, sbom, version())
manifest = checksums([*built, sbom, plugin])
evidence = {
"schema": "ragops-local-release-evidence-0.1",
Expand Down
110 changes: 110 additions & 0 deletions tests/test_local_release.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import importlib.util
import json
import subprocess
import zipfile
from pathlib import Path

import pytest

SPEC = importlib.util.spec_from_file_location("local_release", Path("scripts/local_release.py"))
assert SPEC and SPEC.loader
local_release = importlib.util.module_from_spec(SPEC)
Expand Down Expand Up @@ -65,3 +70,108 @@ def test_environment_tool_prefers_active_python_scripts_directory(tmp_path, monk
monkeypatch.setattr(local_release.sys, "executable", str(python))

assert local_release.environment_tool("ruff") == str(ruff)


def test_sbom_environment_uses_wheel_metadata_without_pip(tmp_path: Path) -> None:
wheel = tmp_path / "ragops-2.0.2-py3-none-any.whl"
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr("ragops/__init__.py", "__version__ = '2.0.2'\n")
archive.writestr(
"ragops-2.0.2.dist-info/METADATA",
"Metadata-Version: 2.1\nName: ragops\nVersion: 2.0.2\n",
)

python = local_release.prepare_sbom_environment(wheel, tmp_path / "sbom-venv")
result = subprocess.run(
[
str(python),
"-c",
(
"import importlib.metadata, importlib.util; "
"print(importlib.metadata.version('ragops')); "
"print(importlib.util.find_spec('pip') is None)"
),
],
check=True,
capture_output=True,
text=True,
)

assert result.stdout.splitlines() == ["2.0.2", "True"]


def test_sbom_validation_rejects_local_build_paths(tmp_path: Path) -> None:
sbom = tmp_path / "sbom.json"
sbom.write_text(
json.dumps(
{
"metadata": {
"component": {
"bom-ref": "root-component",
"name": "ragops",
"version": "2.0.2",
"externalReferences": [
{
"type": "distribution",
"url": "file:///private/build/ragops",
}
],
}
},
"components": [],
"dependencies": [{"ref": "root-component"}],
}
),
encoding="utf-8",
)

with pytest.raises(SystemExit, match="local file URL"):
local_release.validate_sbom(sbom, "2.0.2")


def test_sbom_validation_rejects_wrong_root_version(tmp_path: Path) -> None:
sbom = tmp_path / "sbom.json"
sbom.write_text(
json.dumps(
{
"metadata": {
"component": {
"bom-ref": "root-component",
"name": "ragops",
"version": "2.0.1",
}
},
"components": [],
"dependencies": [{"ref": "root-component"}],
}
),
encoding="utf-8",
)

with pytest.raises(SystemExit, match="root component"):
local_release.validate_sbom(sbom, "2.0.2")


def test_sbom_validation_accepts_dependency_free_release(tmp_path: Path) -> None:
sbom = tmp_path / "sbom.json"
sbom.write_text(
json.dumps(
{
"metadata": {
"component": {
"bom-ref": "root-component",
"name": "ragops",
"version": "2.0.2",
"externalReferences": [
{"type": "website", "url": "https://github.com/thangldw/ragops"}
],
}
},
"components": [],
"dependencies": [{"ref": "root-component"}],
}
),
encoding="utf-8",
)

local_release.validate_sbom(sbom, "2.0.2")