From d902fdda26864b5fc9067b6a267671afda1d0378 Mon Sep 17 00:00:00 2001 From: vaaraio <267591518+vaaraio@users.noreply.github.com> Date: Fri, 22 May 2026 06:46:14 +0300 Subject: [PATCH 1/4] release(v0.27.0): ClusterFuzzLite scaffolding + from_yaml OSError fix Wires ClusterFuzzLite into CI under AddressSanitizer and UndefinedBehaviorSanitizer for the three parsers that ingest attacker-controlled bytes: the OVERT envelope CBOR decoder, the audit-record from_dict deserialiser, and the policy YAML/JSON loader. PR-triggered (300s, code-change mode), nightly batch (3600s), and push-to-main build sanity all land as separate workflows so a broken Dockerfile surfaces immediately rather than at next PR. Bundled fix: from_yaml previously leaked OSError(ENAMETOOLONG) past its PolicyError contract on single-line YAML strings longer than the OS path limit. Path(source).is_file() raised before any handler ran. The is_file probe is now wrapped so any stat failure is interpreted as "not a path" and the input falls through to YAML parsing. Found by the local smoke run of fuzz_policy_loader.py before any atheris fuzzing ran. Regression test pinned in test_policy.py. Full pytest suite: 771 passed, 12 skipped. --- .clusterfuzzlite/Dockerfile | 17 ++++++ .clusterfuzzlite/build.sh | 12 ++++ .github/workflows/cflite_batch.yml | 31 ++++++++++ .github/workflows/cflite_cifuzz.yml | 30 ++++++++++ .github/workflows/cflite_pr.yml | 42 ++++++++++++++ CHANGELOG.md | 58 +++++++++++++++++++ SECURITY.md | 11 ++++ clients/ts/package.json | 2 +- fuzz/fuzz_audit_from_dict.py | 58 +++++++++++++++++++ fuzz/fuzz_overt_envelope.py | 88 +++++++++++++++++++++++++++++ fuzz/fuzz_policy_loader.py | 63 +++++++++++++++++++++ pyproject.toml | 2 +- src/vaara/policy/loader.py | 12 +++- tests/test_policy.py | 17 ++++++ 14 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 .clusterfuzzlite/Dockerfile create mode 100755 .clusterfuzzlite/build.sh create mode 100644 .github/workflows/cflite_batch.yml create mode 100644 .github/workflows/cflite_cifuzz.yml create mode 100644 .github/workflows/cflite_pr.yml create mode 100644 fuzz/fuzz_audit_from_dict.py create mode 100644 fuzz/fuzz_overt_envelope.py create mode 100644 fuzz/fuzz_policy_loader.py diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 00000000..1346138a --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,17 @@ +# ClusterFuzzLite builder image for Vaara fuzz targets. +# +# Base image is OSS-Fuzz's `base-builder-python`, which already carries +# atheris + compile_python_fuzzer plumbing. We layer Vaara's source tree +# on top and install with the [yaml,attestation] extras so the policy +# loader and OVERT envelope targets exercise the real PyYAML / cbor2 +# / cryptography code paths. +FROM gcr.io/oss-fuzz-base/base-builder-python + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +COPY . $SRC/vaara +WORKDIR $SRC/vaara + +COPY .clusterfuzzlite/build.sh $SRC/build.sh diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100755 index 00000000..8255ce18 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash -eu +# ClusterFuzzLite build script for Vaara fuzz targets. +# +# Installs Vaara with the optional extras the fuzz targets actually touch +# (attestation = cbor2 + cryptography; yaml = pyyaml), then compiles each +# `fuzz/fuzz_*.py` target with `compile_python_fuzzer` from base-builder. + +pip3 install --no-cache-dir ".[attestation,yaml]" + +for fuzzer in "$SRC/vaara/fuzz/"fuzz_*.py; do + compile_python_fuzzer "$fuzzer" +done diff --git a/.github/workflows/cflite_batch.yml b/.github/workflows/cflite_batch.yml new file mode 100644 index 00000000..0bd4d652 --- /dev/null +++ b/.github/workflows/cflite_batch.yml @@ -0,0 +1,31 @@ +name: ClusterFuzzLite batch fuzzing + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: read-all + +jobs: + BatchFuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined] + steps: + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: python + sanitizer: ${{ matrix.sanitizer }} + - name: Run fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 3600 + mode: batch + sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/cflite_cifuzz.yml b/.github/workflows/cflite_cifuzz.yml new file mode 100644 index 00000000..d16610f3 --- /dev/null +++ b/.github/workflows/cflite_cifuzz.yml @@ -0,0 +1,30 @@ +name: ClusterFuzzLite continuous build + +on: + push: + branches: [main] + paths: + - src/** + - fuzz/** + - .clusterfuzzlite/** + - .github/workflows/cflite_cifuzz.yml + - pyproject.toml + workflow_dispatch: + +permissions: read-all + +jobs: + Build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined] + steps: + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: python + sanitizer: ${{ matrix.sanitizer }} + upload-build: true diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 00000000..820525a7 --- /dev/null +++ b/.github/workflows/cflite_pr.yml @@ -0,0 +1,42 @@ +name: ClusterFuzzLite PR fuzzing + +on: + pull_request: + branches: [main] + paths: + - src/** + - fuzz/** + - .clusterfuzzlite/** + - .github/workflows/cflite_pr.yml + - pyproject.toml + +permissions: read-all + +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined] + steps: + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + # v1 tag — ClusterFuzzLite ships its action set under this moving ref; + # see https://google.github.io/clusterfuzzlite/build-integration/#step-3-create-the-github-actions-workflows + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: python + sanitizer: ${{ matrix.sanitizer }} + - name: Run fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 + mode: code-change + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ec5694..07f5fd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,64 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] +## [0.27.0] - 2026-05-22 + +**Theme: continuous fuzzing on the parsers that ingest attacker-controlled +input.** The OVERT envelope decoder, the audit-record `from_dict` +deserialiser, and the policy YAML/JSON loader all sit at the boundary +between Vaara and untrusted bytes. A crash, hang, or unhandled exception +in any of them is a denial-of-service vector at minimum, and a +deserialisation hazard at worst. This release wires ClusterFuzzLite into +CI so those three parsers get continuously fuzzed on every PR and nightly +in batch, and ships the first finding the local smoke test caught +(`from_yaml` leaking `OSError(ENAMETOOLONG)` past the `PolicyError` +contract). + +### Added +- `fuzz/fuzz_overt_envelope.py`: atheris target that decodes attacker + CBOR bytes, validates the closed 9-field schema, reconstructs a + `BaseEnvelope`, and signature-checks against a dummy pubkey. Mirrors + the attack surface of `vaara overt verify`. +- `fuzz/fuzz_audit_from_dict.py`: atheris target for `AuditRecord.from_dict` + + `compute_hash()` + the `narrative` property. Models the JSONL-replay + path where trail records get reloaded from disk. +- `fuzz/fuzz_policy_loader.py`: atheris target for `from_json` and + `from_yaml`. Exercises both text paths with attacker-controlled strings. +- `.clusterfuzzlite/Dockerfile` and `.clusterfuzzlite/build.sh`: builder + image based on `gcr.io/oss-fuzz-base/base-builder-python`, installs + Vaara with `[attestation,yaml]` extras, compiles each `fuzz/fuzz_*.py` + target with `compile_python_fuzzer`. +- `.github/workflows/cflite_pr.yml`: PR-triggered fuzzing for 300s under + both address and undefined sanitizers, code-change mode. +- `.github/workflows/cflite_batch.yml`: nightly cron, 3600s batch fuzzing + under both sanitizers. +- `.github/workflows/cflite_cifuzz.yml`: build-sanity on push to main, so + a broken Dockerfile or build.sh surfaces immediately rather than at + next PR. +- `tests/test_policy.py`: + `test_from_yaml_oversize_single_line_treated_as_content_not_path` + pins the regression behaviour of the loader fix below. +- SECURITY.md: brief note on the continuous-fuzzing posture so reporters + know the parsers are under active fuzz coverage. + +### Fixed +- `vaara.policy.loader.from_yaml`: a single-line YAML string longer than + the OS path limit (~255 bytes on most filesystems) previously caused + `Path(source).is_file()` to raise `OSError(ENAMETOOLONG)` directly, + bypassing the loader's `PolicyError` contract. Any caller that loads + YAML from attacker-controlled config could be DoS'd by an oversized + single-line payload. The is_file probe is now wrapped: any stat + failure is interpreted as "not a path" and the input falls through to + YAML parsing, where it surfaces as a normal `PolicyError`. Found by + the local smoke run of `fuzz_policy_loader.py` before any atheris + fuzzing ran. + +### Unchanged +- Hash chain format, OVERT envelope schema, MCP proxy perimeter semantics, + CLI surface, HTTP API, release.yml SLSA provenance. The fuzz targets, + Dockerfile, and CFLite workflows are additive supply-chain + infrastructure; nothing in the runtime kernel moves. + ## [0.26.0] - 2026-05-21 **Theme: per-article verdict drill-down inside the compliance report.** A diff --git a/SECURITY.md b/SECURITY.md index 5898d7d9..3218a5ee 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -67,6 +67,17 @@ receive patches at our discretion for severe issues. - When deploying signed audit exports, protect the signing private key using OS-level key management (hardware-backed keystore or HSM recommended). +## Continuous Fuzzing + +The parsers that ingest attacker-controlled bytes — the OVERT envelope +CBOR decoder, the audit-record `from_dict` deserialiser, and the policy +YAML/JSON loader — are covered by ClusterFuzzLite under both AddressSanitizer +and UndefinedBehaviorSanitizer. PRs that touch `src/`, `fuzz/`, +`.clusterfuzzlite/`, or the CFLite workflows trigger short fuzz runs as a +status check; a nightly cron runs a longer batch. Fuzz target sources +live in `fuzz/fuzz_*.py`. Reports for crashes found by fuzzing should +follow the same private-disclosure path as any other vulnerability. + ## References - OWASP Top 10: diff --git a/clients/ts/package.json b/clients/ts/package.json index 6ea5f2a3..2bf035b7 100644 --- a/clients/ts/package.json +++ b/clients/ts/package.json @@ -1,6 +1,6 @@ { "name": "@vaara/client", - "version": "0.26.0", + "version": "0.27.0", "description": "TypeScript client for the Vaara HTTP API. Conformal risk scoring, hash-chained audit, policy reload, named detectors.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/fuzz/fuzz_audit_from_dict.py b/fuzz/fuzz_audit_from_dict.py new file mode 100644 index 00000000..baf14c4f --- /dev/null +++ b/fuzz/fuzz_audit_from_dict.py @@ -0,0 +1,58 @@ +"""Atheris fuzz target: AuditRecord.from_dict. + +Models the JSONL-replay path: trail records on disk get reloaded by +deserialising untrusted JSON into `AuditRecord.from_dict`. Bad input +must surface as `TypeError`/`ValueError`/`KeyError` — never silently +construct a corrupt record and never crash the process. + +After deserialisation the target also exercises `compute_hash()` and +the `narrative` property — both are reached when a regulator replays +a trail, and both have already had defensive fixes (NaN/overflow +timestamps, oversize agent_id) that fuzzing should keep honest. +""" + +from __future__ import annotations + +import json +import sys + +import atheris + +with atheris.instrument_imports(): + from vaara.audit.trail import AuditRecord + + +def TestOneInput(data: bytes) -> None: + fdp = atheris.FuzzedDataProvider(data) + raw = fdp.ConsumeUnicode(sys.maxsize) + try: + parsed = json.loads(raw) + except (json.JSONDecodeError, ValueError, RecursionError): + return + + if not isinstance(parsed, dict): + return + + try: + record = AuditRecord.from_dict(parsed) + except (TypeError, ValueError, KeyError): + return + + try: + record.compute_hash() + except (TypeError, ValueError): + return + + try: + _ = record.narrative + except (TypeError, ValueError): + return + + +def main() -> None: + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/fuzz_overt_envelope.py b/fuzz/fuzz_overt_envelope.py new file mode 100644 index 00000000..b9f0e562 --- /dev/null +++ b/fuzz/fuzz_overt_envelope.py @@ -0,0 +1,88 @@ +"""Atheris fuzz target: OVERT 1.0 envelope CBOR decode + verify. + +Mirrors the attack surface of `vaara overt verify`: attacker-controlled CBOR +bytes that get loaded by `cbor2.loads`, structurally validated against the +closed 9-field schema, mapped into a `BaseEnvelope`, then signature-checked. + +A finding is anything that escapes the documented error contract: any +exception other than the ones the verifier path is expected to swallow +(`EnvelopeError`, `CBORDecodeError`, `TypeError`, `ValueError`, `KeyError`, +`OverflowError`, `MemoryError`). Hangs and uncaught crashes count too. +""" + +from __future__ import annotations + +import sys + +import atheris + +with atheris.instrument_imports(): + import cbor2 + from cbor2 import CBORDecodeError + + from vaara.attestation.overt import ( + BaseEnvelope, + EnvelopeError, + verify_base_envelope, + ) + +_REQUIRED_KEYS = ( + "blinded_identifier", + "request_commitment", + "encoder_binary_identity", + "non_content_metadata", + "monotonic_counter", + "nanosecond_timestamp", + "key_identifier", + "arbiter_instance_identifier", + "signature", +) + +# Fixed 32-byte all-zero pubkey: cheap, deterministic, never collides with +# a real key_identifier so verify_base_envelope must always reach the +# signature path or reject earlier. +_DUMMY_PUBKEY = b"\x00" * 32 + + +def TestOneInput(data: bytes) -> None: + try: + decoded = cbor2.loads(data) + except (CBORDecodeError, MemoryError, OverflowError, ValueError): + return + except Exception: + # Anything else from the CBOR decoder is interesting. + raise + + if not isinstance(decoded, dict): + return + if any(k not in decoded for k in _REQUIRED_KEYS): + return + + try: + envelope = BaseEnvelope( + blinded_identifier=decoded["blinded_identifier"], + request_commitment=decoded["request_commitment"], + encoder_binary_identity=decoded["encoder_binary_identity"], + non_content_metadata=decoded["non_content_metadata"], + monotonic_counter=int(decoded["monotonic_counter"]), + nanosecond_timestamp=int(decoded["nanosecond_timestamp"]), + key_identifier=decoded["key_identifier"], + arbiter_instance_identifier=decoded["arbiter_instance_identifier"], + signature=decoded["signature"], + ) + except (TypeError, ValueError, OverflowError, KeyError): + return + + try: + verify_base_envelope(envelope, _DUMMY_PUBKEY) + except (EnvelopeError, TypeError, ValueError): + return + + +def main() -> None: + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/fuzz_policy_loader.py b/fuzz/fuzz_policy_loader.py new file mode 100644 index 00000000..5530d53a --- /dev/null +++ b/fuzz/fuzz_policy_loader.py @@ -0,0 +1,63 @@ +"""Atheris fuzz target: policy JSON/YAML loader. + +The policy loader is what turns regulator/operator-supplied YAML or JSON +into the in-memory `Policy` that gates every action at runtime. A loader +that crashes on hostile input is a denial-of-service surface; a loader +that silently produces a malformed `Policy` is worse. + +This target fuzzes the `from_json` text path (which is what file loads +funnel into) plus the `from_yaml` text path when PyYAML is importable. +Expected exceptions: `PolicyError`, `TypeError`, `ValueError`, `KeyError`. +""" + +from __future__ import annotations + +import sys + +import atheris + +with atheris.instrument_imports(): + from vaara.policy.loader import from_json, from_yaml + from vaara.policy.schema import PolicyError + +try: + import yaml as _yaml # noqa: F401 + + _HAS_YAML = True +except ImportError: + _HAS_YAML = False + + +def TestOneInput(data: bytes) -> None: + fdp = atheris.FuzzedDataProvider(data) + choice = fdp.ConsumeIntInRange(0, 1) if _HAS_YAML else 0 + text = fdp.ConsumeUnicode(sys.maxsize) + + if choice == 0: + try: + from_json(text) + except (PolicyError, TypeError, ValueError, KeyError, RecursionError): + return + else: + try: + from_yaml(text) + except (PolicyError, TypeError, ValueError, KeyError, RecursionError): + return + except Exception as exc: + # PyYAML raises subclasses of yaml.YAMLError on parse failure. + # The loader is supposed to wrap those into PolicyError; anything + # else leaking through is a finding. + import yaml + + if isinstance(exc, yaml.YAMLError): + return + raise + + +def main() -> None: + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 91e64127..331fbc25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vaara" -version = "0.26.0" +version = "0.27.0" description = "Adaptive AI Agent Execution Layer for risk scoring, audit trails, and regulatory compliance" requires-python = ">=3.10" license = "Apache-2.0" diff --git a/src/vaara/policy/loader.py b/src/vaara/policy/loader.py index fb90ee48..2947cfca 100644 --- a/src/vaara/policy/loader.py +++ b/src/vaara/policy/loader.py @@ -260,8 +260,16 @@ def from_yaml(source: Union[str, Path]) -> Policy: if isinstance(source, Path): text = _read_policy_text(source) - elif isinstance(source, str) and "\n" not in source and Path(source).is_file(): - text = _read_policy_text(Path(source)) + elif isinstance(source, str) and "\n" not in source: + # An attacker-controlled string longer than the OS path limit makes + # Path(...).is_file() raise OSError(ENAMETOOLONG) directly, bypassing + # this loader's PolicyError contract. Treat any stat() failure as + # "not a path" and fall through to YAML parsing. + try: + looks_like_path = Path(source).is_file() + except OSError: + looks_like_path = False + text = _read_policy_text(Path(source)) if looks_like_path else source else: text = source diff --git a/tests/test_policy.py b/tests/test_policy.py index 8ee4649a..01111e2f 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -308,3 +308,20 @@ def test_from_yaml_path(tmp_path: Path) -> None: f.write_text(yaml.safe_dump(MINIMAL_POLICY)) p = from_yaml(f) assert p.version == SCHEMA_VERSION + + +def test_from_yaml_oversize_single_line_treated_as_content_not_path() -> None: + """A single-line YAML string longer than the OS path limit must not leak + OSError(ENAMETOOLONG) from the internal Path(...).is_file() probe. + + Regression: the loader previously called Path(source).is_file() before + any exception handler ran, so a >255-byte single-line attacker input + bypassed the PolicyError contract and raised a bare OSError. Now any + stat() failure on the path-probe is interpreted as "not a path" and the + string falls through to the YAML parser, which then surfaces as a + PolicyError on invalid YAML or a malformed-policy error on valid YAML. + """ + pytest.importorskip("yaml") + oversize = "a: " * 500 + with pytest.raises(PolicyError): + from_yaml(oversize) From ed5c338a990637c1d0474380d0f3f8e2c1e8619c Mon Sep 17 00:00:00 2001 From: vaaraio <267591518+vaaraio@users.noreply.github.com> Date: Fri, 22 May 2026 07:11:51 +0300 Subject: [PATCH 2/4] fix(release.yml): swap publish-npm back to NPM_TOKEN auth npm Trusted Publishing has been 404'ing on every release tag since v0.23.0. The TP form on the package access page kept clearing on save (or never persisting in the first place), so the OIDC token had nothing to match against on npm side. Every release after v0.23 has required the manual `npm login --auth-type=web` + `npm publish --no-provenance` fallback to get the TypeScript client out the door. This swaps the publish-npm job back to NODE_AUTH_TOKEN with the NPM_TOKEN repo secret, drops --provenance from the publish command, and removes the now-unused id-token: write permission. The secret has been rotated to a token with "Allow bypass 2FA when publishing" enabled. Tradeoff accepted: lose npm-side provenance attestation on the @vaara/client artefact. SLSA build provenance via actions/attest-build-provenance still attests the wheel, sdist, and GitHub Release artefacts. The TypeScript client is a thin HTTP wrapper, not security-critical surface. --- .github/workflows/release.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c2c101f..d473873b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,6 @@ jobs: if: ${{ vars.NPM_PUBLISH_ENABLED == 'true' }} permissions: contents: read - id-token: write # required for npm provenance defaults: run: working-directory: clients/ts @@ -129,5 +128,13 @@ jobs: - name: Test run: node --test test/*.test.mjs - - name: Publish to npm with provenance - run: npm publish --provenance --access public + - name: Publish to npm + # Auth via NPM_TOKEN, no --provenance. Trusted Publishing OIDC was + # configured for v0.25 but kept 404ing on every release after a clean + # workflow filename save, so we fell back to the token path that + # ships reliably. SLSA build provenance still attests to the wheel, + # sdist, and GitHub Release artefacts via attest-build-provenance, + # only the npm-side attestation is dropped. + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public From 5c1afb5052560f24f6189c56dc7747e5350a5fb0 Mon Sep 17 00:00:00 2001 From: vaaraio <267591518+vaaraio@users.noreply.github.com> Date: Fri, 22 May 2026 07:22:32 +0300 Subject: [PATCH 3/4] Revert "fix(release.yml): swap publish-npm back to NPM_TOKEN auth" This reverts commit ed5c338a990637c1d0474380d0f3f8e2c1e8619c. --- .github/workflows/release.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d473873b..0c2c101f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,6 +106,7 @@ jobs: if: ${{ vars.NPM_PUBLISH_ENABLED == 'true' }} permissions: contents: read + id-token: write # required for npm provenance defaults: run: working-directory: clients/ts @@ -128,13 +129,5 @@ jobs: - name: Test run: node --test test/*.test.mjs - - name: Publish to npm - # Auth via NPM_TOKEN, no --provenance. Trusted Publishing OIDC was - # configured for v0.25 but kept 404ing on every release after a clean - # workflow filename save, so we fell back to the token path that - # ships reliably. SLSA build provenance still attests to the wheel, - # sdist, and GitHub Release artefacts via attest-build-provenance, - # only the npm-side attestation is dropped. - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish --access public + - name: Publish to npm with provenance + run: npm publish --provenance --access public From 018a91b5a4f4550ac083616fddbd8932f84bc4db Mon Sep 17 00:00:00 2001 From: vaaraio <267591518+vaaraio@users.noreply.github.com> Date: Fri, 22 May 2026 08:38:42 +0300 Subject: [PATCH 4/4] chore(readme): reorganize for scannability and clearer positioning - Numbers section gets a lede with the three headline stats and moves above Install - Three integration prose sections compressed into two tables (framework adapters, upstream-signal adapters) - Long capability paragraphs in the MCP proxy section collapsed into
- 'Per-article verdict drill-down' paragraph collapsed into
- New 'Who reaches for Vaara' section between Quick start and evidence output - OVERT 1.0 section reframed as What / Why / How - MS Agent Governance Toolkit framing inverted: orchestration toolkits sit on top of Vaara - IMDA MGF v1.5 acknowledgement link added next to the Apply AI Alliance link - License section names Apache 2.0 explicitly - 'since vX.Y.Z' framing removed throughout; CHANGELOG owns version evolution --- README.md | 120 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5b1c6551..b25272b8 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,21 @@ Vaara is the runtime evidence layer for AI Act compliance. Open source, no SaaS, no telemetry. -Vaara intercepts agent tool calls, scores each one with a conformal risk interval, and writes a hash-chained audit record. Online learning across five expert signals via Multiplicative Weight Update. Distribution-free conformal coverage on the score. +Vaara intercepts agent tool calls, scores each one with a conformal risk interval, and writes a hash-chained audit record. Online learning across five expert signals via Multiplicative Weight Update. Distribution-free conformal coverage on the score. An external auditor can verify these properties without trusting your stack. Orchestration toolkits and identity layers (Microsoft Agent Governance Toolkit, others) sit on top. -In practical terms: every agent action gets a confidence-bounded risk score with mathematical coverage guarantees, the scorer learns from its misses, and the audit record is tamper-evident. An external auditor can verify these properties without trusting your stack. +## Numbers -For broader agent governance (zero-trust identity, capability-based access control, multi-language SDKs) see Microsoft's [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit). +97.1% attack recall on a held-out distribution-shift split. 140 µs p99 inference latency on commodity CPU. Zero attack success against a PAIR adaptive attacker over 25 attempts. + +- 5,955-entry adversarial corpus (3,422 attack across 8 categories, 2,533 benign) +- 97.1% attack recall on held-out distribution-shift split, threshold 0.55 +- PAIR adaptive-attacker calibration: ASR 0/25 against Qwen2.5-32B +- [vaara-bench-v1](bench/vaara-bench-v1.md): 77-trace synthetic-corpus benchmark with frozen methodology, 100% soft TPR, 0% hard FPR +- 140 µs / 210 µs p99 inference latency, commodity CPU +- Distribution-free conformal coverage on the score +- MWU regret bound O(sqrt(T log N)) + +Each figure is reproducible from the public corpus or the bench harness in `bench/`. ## Install @@ -50,6 +60,13 @@ else: `report_outcome` closes the loop. MWU reweights signals based on which ones predicted the outcome. +## Who reaches for Vaara + +- **AI compliance teams** shipping high-risk systems under the EU AI Act — Article 9 risk management, Article 12 logging, Article 15 robustness, Article 61 post-market monitoring evidence. +- **ML platform teams** adding runtime governance to agentic stacks (LangChain, CrewAI, OpenAI Agents SDK, MCP-based hosts) without rewriting orchestration. +- **AI safety and red teams** calibrating scorers against adaptive attackers (PAIR, distribution-shift evals, custom corpora). +- **Notified Bodies and internal auditors** reading article-level evidence reports without trusting the deployer's stack. + ## What evidence looks like `vaara compliance report --format json` against a real audit trail produces an article-level evidence record an auditor can read directly. Status is reported honestly: articles without recorded events return `evidence_insufficient`, not a rubber-stamp. @@ -72,51 +89,40 @@ else: The same data renders as a styled PDF for Notified Bodies (`vaara compliance report --format pdf`, requires `pip install 'vaara[pdf]'`), a static HTML dashboard (`vaara compliance dashboard`), or a Sigstore-signed regulator-handoff envelope (`vaara trail export`, optional ML-DSA-65 / FIPS 204 post-quantum signer via `pip install 'vaara[pq]'`). -**Per-article verdict drill-down** (since v0.26.0). Each article in the report now carries two extra surfaces a reviewer can read without re-running the engine. `verdict_inputs` lists the threshold-vs-observed snapshot the engine compared against (minimum record count, staleness window, strong-strength bounds, future-timestamp and chain-integrity flags) plus a `verdict_reasons` list of human-readable rationale lines explaining why the status and strength landed where they did. `contributing_events` lists the most recent qualifying audit records the verdict sits on (record ID, action ID, ISO timestamp, agent, tool, and a filtered `drill_down` dict of just the data fields that fed the risk/decision/outcome: point estimate, conformal interval, decision, reason, outcome severity). The drill-down renders in every output format: JSON, markdown, narrative, PDF, and the HTML dashboard. An auditor reading the report can trace `status → threshold delta → concrete event` in one sitting. - -## Numbers - -Concrete evidence the system performs at levels relevant to high-risk-use deployments. Each figure is reproducible from the public corpus or the bench harness in `bench/`. +
+Per-article verdict drill-down -- 5,955-entry adversarial corpus (3,422 attack across 8 categories, 2,533 benign) -- 97.1% attack recall on held-out distribution-shift split, threshold 0.55 -- PAIR adaptive-attacker calibration: ASR 0/25 against Qwen2.5-32B -- [vaara-bench-v1](bench/vaara-bench-v1.md): 77-trace synthetic-corpus benchmark with frozen methodology, 100% soft TPR, 0% hard FPR -- 140 µs / 210 µs p99 inference latency, commodity CPU -- Distribution-free conformal coverage on the score -- MWU regret bound O(sqrt(T log N)) - -## Framework integrations +Each article in the report carries two extra surfaces a reviewer can read without re-running the engine. `verdict_inputs` lists the threshold-vs-observed snapshot the engine compared against (minimum record count, staleness window, strong-strength bounds, future-timestamp and chain-integrity flags) plus a `verdict_reasons` list of human-readable rationale lines explaining why the status and strength landed where they did. `contributing_events` lists the most recent qualifying audit records the verdict sits on (record ID, action ID, ISO timestamp, agent, tool, and a filtered `drill_down` dict of just the data fields that fed the risk/decision/outcome: point estimate, conformal interval, decision, reason, outcome severity). The drill-down renders in every output format: JSON, markdown, narrative, PDF, and the HTML dashboard. An auditor reading the report can trace `status → threshold delta → concrete event` in one sitting. +
-Native adapters in `src/vaara/integrations/` route the major Python agent frameworks through Vaara's pipeline. Each adapter intercepts tool calls via the framework's own callback or hook surface, scores them, gates them, and emits the same audit events as a direct `pipeline.intercept()` call. Frameworks are not hard dependencies (lazy import, duck typing), so the base `pip install vaara` keeps a clean dependency tree. +## Framework adapters -- **LangChain.** `VaaraCallbackHandler` slots into `config={"callbacks": [...]}` and gates every tool invocation automatically. `vaara_wrap_tool(tool, pipeline)` is the per-tool variant for fine-grained control. -- **CrewAI.** `VaaraCrewGovernance` wraps a crew so every agent action passes through the same scoring and audit chain. -- **OpenAI Agents SDK.** `VaaraToolGuardrail` plus `vaara_wrap_function` wrap function-tool calls before they execute. Compatible with the Responses API and the Agents-SDK tracing model. -- **MCP server.** `vaara.integrations.mcp_server` exposes scoring, audit emission, and policy reload as MCP tools so any MCP-compatible agent can route through Vaara without a custom client. (For Vaara *in front of* an upstream MCP server, see the [MCP proxy](#mcp-proxy-vaara-as-a-transparent-governance-layer) section below.) +Native adapters in `src/vaara/integrations/` route the major Python agent frameworks through Vaara's pipeline. Each intercepts via the framework's own callback or hook surface, scores, gates, and emits the same audit events as a direct `pipeline.intercept()`. Frameworks are not hard dependencies (lazy import, duck typing). -All four framework adapters share the same in-process pipeline, so audit records hash-chain together regardless of which framework the action came through. Each adapter has its own docstring with the two integration patterns it supports. +| Framework | Entry point | Use | +|---|---|---| +| LangChain | `VaaraCallbackHandler`, `vaara_wrap_tool` | Slots into `config={"callbacks": [...]}` or wraps per-tool | +| CrewAI | `VaaraCrewGovernance` | Wraps a crew so every agent action passes through scoring + audit | +| OpenAI Agents SDK | `VaaraToolGuardrail`, `vaara_wrap_function` | Function-tool wrap, compatible with Responses API and Agents-SDK tracing | +| MCP server | `vaara.integrations.mcp_server` | Exposes scoring, audit, policy reload as MCP tools | -### Cloud guardrails as upstream signals +All four share the same in-process pipeline, so audit records hash-chain together regardless of which framework the action came through. For Vaara *in front of* an upstream MCP server, see the [MCP proxy](#mcp-proxy-vaara-as-a-transparent-governance-layer) section below. -Three adapters route findings from AWS Bedrock Guardrails, Azure AI Content Safety, and GCP Model Armor into Vaara's audit trail and OVERT envelope with EU AI Act article tags. The cloud filter runs in the deployer's environment as an upstream signal. Vaara records the verdict, normalises 27 provider categories onto a shared vocabulary, and tags each finding against Art. 5, 10, 13, 15, 53, and the CSAM-specific obligation from the Digital Omnibus political agreement of May 2026. +## Upstream-signal adapters (cloud + OSS guardrails) -- **AWS Bedrock Guardrails.** `vaara.integrations.bedrock_guardrails.BedrockGuardrailsAdapter`. Wraps `ApplyGuardrail` across the five Bedrock policy buckets. -- **Azure AI Content Safety.** `vaara.integrations.azure_content_safety.AzureContentSafetyAdapter`. Wraps `analyze_text`, Prompt Shields, Protected Material, and Groundedness Detection. -- **GCP Model Armor.** `vaara.integrations.gcp_model_armor.GcpModelArmorAdapter`. Wraps `sanitize_user_prompt` and `sanitize_model_response`. +Adapters route findings from cloud and OSS guardrails into Vaara's audit trail and OVERT envelope with EU AI Act article tags. The filter runs in the deployer's environment as an upstream signal. Vaara records the verdict, normalises 68 provider categories onto a shared vocabulary, and tags each finding against Art. 5, 10, 13, 15, 53, and the CSAM-specific obligation from the Digital Omnibus political agreement of May 2026. -Each adapter returns a `ContentSafetyFinding` the deployer routes into `pipeline.intercept(context=finding.to_audit_context())`. Cloud SDKs are optional extras: `pip install 'vaara[bedrock]'`, `pip install 'vaara[azure-content-safety]'`, `pip install 'vaara[gcp-model-armor]'`. The category-to-article mapping table lives in `src/vaara/integrations/_content_safety_articles.py` and is the value the adapters wrap. Article-level rationale is in [COMPLIANCE.md](COMPLIANCE.md#cloud-guardrail-adapter-pattern). +| Provider | Adapter | Extra | Wraps | +|---|---|---|---| +| AWS Bedrock Guardrails | `BedrockGuardrailsAdapter` | `vaara[bedrock]` | `ApplyGuardrail` across five Bedrock policy buckets | +| Azure AI Content Safety | `AzureContentSafetyAdapter` | `vaara[azure-content-safety]` | `analyze_text`, Prompt Shields, Protected Material, Groundedness | +| GCP Model Armor | `GcpModelArmorAdapter` | `vaara[gcp-model-armor]` | `sanitize_user_prompt`, `sanitize_model_response` | +| NVIDIA NeMo Guardrails | `NemoGuardrailsAdapter` | `vaara[nemo-guardrails]` | `GenerationResponse.log.activated_rails` (input / dialog / output / retrieval) | +| Guardrails AI | `GuardrailsAIAdapter` | `vaara[guardrails-ai]` | `ValidationOutcome.validation_summaries` from `Guard.parse` / `Guard.validate` | +| LLM Guard | `LLMGuardAdapter` | `vaara[llm-guard]` | `scan_prompt` / `scan_output`, parses `(sanitized, results_valid, results_score)` | +| Rebuff | `RebuffAdapter` | `vaara[rebuff]` | `DetectResponse` across heuristic, model, vector layers + canary-word leak check | -### OSS guardrails as upstream signals - -Four adapters route findings from NVIDIA NeMo Guardrails, Guardrails AI, LLM Guard, and Rebuff into the same audit trail and OVERT envelope path as the v0.19.0 cloud adapters. The OSS guardrail runs in the deployer's environment as an upstream signal. Vaara records the verdict, normalises 41 OSS provider categories onto the same shared vocabulary, and tags each finding against Art. 5, 10, 13, 15, and 53. - -- **NVIDIA NeMo Guardrails**. `vaara.integrations.nemo_guardrails.NemoGuardrailsAdapter`. Parses `GenerationResponse.log.activated_rails` across input, dialog, output, and retrieval rail types. -- **Guardrails AI**. `vaara.integrations.guardrails_ai.GuardrailsAIAdapter`. Parses `ValidationOutcome.validation_summaries` from any `Guard.parse` or `Guard.validate` call. -- **LLM Guard**. `vaara.integrations.llm_guard.LLMGuardAdapter`. Wraps `scan_prompt` and `scan_output` and parses the `(sanitized, results_valid, results_score)` triple. -- **Rebuff**. `vaara.integrations.rebuff.RebuffAdapter`. Parses `DetectResponse` across the heuristic, model, and vector injection-detection layers, plus the canary-word leak check on responses. - -Each adapter returns a `ContentSafetyFinding` the deployer routes into `pipeline.intercept(context=finding.to_audit_context())`. OSS SDKs are optional extras: `pip install 'vaara[nemo-guardrails]'`, `pip install 'vaara[guardrails-ai]'`, `pip install 'vaara[llm-guard]'`, `pip install 'vaara[rebuff]'`. The 41 new mapping rows extend the same table at `src/vaara/integrations/_content_safety_articles.py`. Article-level rationale is in [COMPLIANCE.md](COMPLIANCE.md#oss-guardrail-adapter-pattern). +Each adapter returns a `ContentSafetyFinding` the deployer routes into `pipeline.intercept(context=finding.to_audit_context())`. The mapping table lives at `src/vaara/integrations/_content_safety_articles.py`. Article-level rationale in [COMPLIANCE.md](COMPLIANCE.md#cloud-guardrail-adapter-pattern) and [COMPLIANCE.md](COMPLIANCE.md#oss-guardrail-adapter-pattern). ## HTTP API @@ -154,9 +160,7 @@ if (r.decision === "deny") throw new Error("blocked"); ## MCP proxy (Vaara as a transparent governance layer) -`vaara.integrations.mcp_proxy.VaaraMCPProxy` sits between an MCP client (Claude Code, Cursor, any MCP-capable host) and an upstream MCP server. Every `tools/call` from the client routes through Vaara's interception pipeline before reaching the upstream. Allowed calls forward transparently and report the upstream outcome back to the scorer. Blocked calls return an MCP `isError: true` response with the block reason. The initialization handshake and `notifications/*` forward unchanged. `tools/list`, `resources/list`, `resources/read`, `prompts/list`, and `prompts/get` route through the operator perimeter described below before reaching the client or upstream. - -One-line example in front of [`@sap/mdk-mcp-server`](https://www.npmjs.com/package/@sap/mdk-mcp-server) (SAP's official Mobile Development Kit MCP server, on npm): +`vaara.integrations.mcp_proxy.VaaraMCPProxy` sits between an MCP client (Claude Code, Cursor, any MCP-capable host) and an upstream MCP server. Every `tools/call` from the client routes through Vaara's interception pipeline before reaching the upstream. Allowed calls forward transparently and report the upstream outcome back to the scorer. Blocked calls return an MCP `isError: true` response with the block reason. The initialization handshake and `notifications/*` forward unchanged. `tools/list`, `resources/list`, `resources/read`, `prompts/list`, and `prompts/get` route through the operator perimeter before reaching the client or upstream. ```bash python -m vaara.integrations.mcp_proxy \ @@ -166,13 +170,16 @@ python -m vaara.integrations.mcp_proxy \ Point your MCP client at the proxy instead of the upstream. The audit chain captures every tool call without changing client or upstream behavior. Distinct from `mcp_server`, which exposes Vaara itself as an MCP server for agents that consult Vaara as a tool. -**Operator-side tool filtering** (since v0.22.0). The proxy accepts repeatable `--allow-tool NAME` and `--deny-tool NAME` flags. Filtered tools are dropped from `tools/list` responses before the client sees them and any matching `tools/call` is rejected at the proxy perimeter without contacting the upstream. Use this to hide write/delete tools (e.g. `delete_file`, `merge_pull_request`) when the upstream server exposes more capability than the deployment policy allows. Denylist wins on overlap with allowlist. No flags = passthrough. +
+Operator perimeter: tool, resource, prompt filtering -**Resources and prompts coverage** (since v0.23.0). The same perimeter shape extends to MCP's other two primitives. `--allow-resource URI` / `--deny-resource URI` gate `resources/list` and `resources/read`. `--allow-prompt NAME` / `--deny-prompt NAME` gate `prompts/list` and `prompts/get`. Every allowed `resources/read` and `prompts/get` writes a request+decision audit pair to the hash chain so a regulator can reconstruct exactly which resources the agent read and which prompts it retrieved. Resource reads and prompt gets are read-oriented MCP surfaces. They do not run through the risk scorer. The operator perimeter is the gate, and the audit chain captures the evidence. +The proxy accepts repeatable `--allow-tool NAME` / `--deny-tool NAME`, `--allow-resource URI` / `--deny-resource URI`, and `--allow-prompt NAME` / `--deny-prompt NAME` flags. Filtered tools are dropped from `tools/list` responses before the client sees them and any matching `tools/call` is rejected at the proxy perimeter without contacting the upstream. The same shape extends to `resources/list` + `resources/read` and `prompts/list` + `prompts/get`. Denylist wins on overlap with allowlist. No flags = passthrough. Every allowed `resources/read` and `prompts/get` writes a request+decision audit pair to the hash chain so a regulator can reconstruct exactly which resources the agent read and which prompts it retrieved. Read-oriented MCP surfaces do not run through the risk scorer. The operator perimeter is the gate, the audit chain is the evidence. +
-**OVERT 1.0 envelopes per interaction** (since v0.24.0). Off by default. When you pass `--overt-signing-key KEY.pem`, `--overt-operator-key OPKEY.bin`, and `--overt-receipts-dir DIR/`, the proxy writes one OVERT 1.0 Protocol Profile 1.0 Base Envelope (canonical CBOR, Ed25519, closed 9-field schema) per governed interaction into `DIR/{nanosecond_timestamp}-{counter:010d}.cbor`. Covers all four states: allowed `tools/call`, blocked `tools/call`, perimeter-filtered call, and perimeter-filtered `resources/read` / `prompts/get`. The arbiter public key is pinned alongside as `pubkey.bin`. Each envelope verifies offline under `vaara overt verify` against any conformant verifier. Three-step recipe: +
+OVERT 1.0 envelopes per interaction -**Streaming notifications inside the boundary** (since v0.25.0). Long-running upstream tools emit `notifications/progress` and `notifications/message` over the lifetime of a `tools/call`. The proxy now routes each notification through the same audit pair (request + decision) and, when OVERT is configured, emits a dedicated Base Envelope with action class `mcp.notification.progress` or `mcp.notification.message`. Progress events correlate to the originating call via the `_meta.progressToken` from the request, so a regulator reading the receipt directory can reconstruct what arrived between request and response. Notifications still forward to the client unchanged. Audit failures are logged and swallowed: observation never blocks streaming. +Off by default. When you pass `--overt-signing-key KEY.pem`, `--overt-operator-key OPKEY.bin`, and `--overt-receipts-dir DIR/`, the proxy writes one OVERT 1.0 Protocol Profile 1.0 Base Envelope (canonical CBOR, Ed25519, closed 9-field schema) per governed interaction into `DIR/{nanosecond_timestamp}-{counter:010d}.cbor`. Covers all four states: allowed `tools/call`, blocked `tools/call`, perimeter-filtered call, and perimeter-filtered `resources/read` / `prompts/get`. The arbiter public key is pinned alongside as `pubkey.bin`. Each envelope verifies offline under `vaara overt verify` against any conformant verifier. ```bash # 1. Generate an Ed25519 signing key (evaluation/demo; for production use a KMS or HSM, see docs/signing-keys.md). @@ -194,7 +201,14 @@ vaara overt verify ./overt_receipts/1779309684224332669-0000000001.cbor \ # → {"valid": true, "monotonic_counter": 1, ...} ``` -`non_content_metadata` carries structural fields only (action class, tool/resource/prompt identifier, decision, reason, agent_id, action_id). The request content itself never leaves the operator environment. Only its HMAC-SHA256 commitment crosses the trust boundary. The monotonic counter advances strictly across the whole proxy process so gaps are detectable. Emission failure is logged and swallowed: attestation problems must not block legitimate upstream traffic. +`non_content_metadata` carries structural fields only (action class, tool/resource/prompt identifier, decision, reason, agent_id, action_id). The request content itself never leaves the operator environment; only its HMAC-SHA256 commitment crosses the trust boundary. The monotonic counter advances strictly across the whole proxy process so gaps are detectable. Emission failure is logged and swallowed: attestation problems must not block legitimate upstream traffic. +
+ +
+Streaming notifications inside the boundary + +Long-running upstream tools emit `notifications/progress` and `notifications/message` over the lifetime of a `tools/call`. The proxy routes each notification through the same audit pair (request + decision) and, when OVERT is configured, emits a dedicated Base Envelope with action class `mcp.notification.progress` or `mcp.notification.message`. Progress events correlate to the originating call via the `_meta.progressToken` from the request, so a regulator reading the receipt directory can reconstruct what arrived between request and response. Notifications still forward to the client unchanged. Audit failures are logged and swallowed: observation never blocks streaming. +
Worked examples with real upstream servers: @@ -205,9 +219,11 @@ The proxy is MCP-protocol-level, not vendor-specific. The same three-step recipe ## OVERT 1.0 attestation -OVERT 1.0 lets an external party (regulator, auditor, customer) verify that a runtime decision actually happened the way you say it did, without trusting your stack or reading your code. Vaara emits OVERT envelopes alongside its audit records. +**What.** OVERT 1.0 is an open standard for runtime trust in AI systems ([overt.is](https://overt.is/), authored by Glacis Technologies, published 25 March 2026). It defines a signed, schema-closed envelope a relying party can verify offline without trusting the emitter. + +**Why.** A regulator, auditor, or customer can confirm that a runtime decision actually happened the way you say it did, without reading your code or trusting your stack. -Vaara implements the OVERT 1.0 ([overt.is](https://overt.is/)) Protocol Profile 1.0 Base Envelope. OVERT 1.0 is an open standard for runtime trust in AI systems, authored by Glacis Technologies and published 25 March 2026. Closed-schema 9-field structure at AAL-3 Phase 2 (Provisional Receipt), canonical CBOR (RFC 8949), Ed25519 signatures, HMAC-SHA256 keyed commitments, IEEE-754 float rejection. +**How Vaara emits it.** Vaara is the **Arbiter** in OVERT terms and ships Protocol Profile 1.0 Base Envelopes (canonical CBOR per RFC 8949, Ed25519 signatures, HMAC-SHA256 keyed commitments, closed 9-field schema, IEEE-754 float rejection) alongside every audit record when attestation is enabled. ``` pip install 'vaara[attestation]' @@ -226,7 +242,7 @@ envelope = emit_base_envelope( ) ``` -Vaara operates as the **Arbiter** in OVERT terms. The reference Phase 3 IAP (`vaara.attestation.iap`) notary-signs the Provisional Receipt and anchors it in a transparency log. Production deployments can swap in sigstore Rekor or an equivalent independently-operated log at the same call sites. The OVERT S3P (MEA-2) emitter at `vaara.attestation.s3p` ships exact Clopper-Pearson confidence intervals (pure Python, no scipy) and a proposed Protocol Profile extension that reports aggregate statistics over per-action conformal prediction intervals alongside the standard binomial CI. +The reference Phase 3 IAP (`vaara.attestation.iap`) notary-signs the Provisional Receipt and anchors it in a transparency log. Production deployments can swap in sigstore Rekor or an equivalent independently-operated log at the same call sites. The OVERT S3P (MEA-2) emitter at `vaara.attestation.s3p` ships exact Clopper-Pearson confidence intervals (pure Python, no scipy) and a proposed Protocol Profile extension that reports aggregate statistics over per-action conformal prediction intervals alongside the standard binomial CI. The `vaara overt verify RECEIPT.cbor --pubkey-file PUB.bin` CLI validates any canonical-CBOR Base Envelope against a supplied raw 32-byte Ed25519 public key. The verifier reads only the wire format and takes no dependency on Vaara's emitter, so any OVERT-conformant implementation can route its conformance check through it. @@ -249,10 +265,10 @@ See [COMPLIANCE.md](COMPLIANCE.md) "Position relative to open runtime-attestatio | `src/vaara/policy/` | YAML / JSON policy schema, `vaara policy validate` and `vaara policy test` | | `src/vaara/sandbox/` | Synthetic-trace cold-start calibration | -[Article 14 runtime: why oversight of agentic AI has to be evidenced as action, not model](https://futurium.ec.europa.eu/ga/apply-ai-alliance/community-content/article-14-runtime-why-oversight-agentic-ai-has-be-evidenced-action-not-model) is the position post on the EU Apply AI Alliance Futurium. +Acknowledgements: Vaara is listed in the industry acknowledgements of the [IMDA Model AI Governance Framework for Agentic AI v1.5](https://www.imda.gov.sg/-/media/imda/files/about/emerging-tech-and-research/artificial-intelligence/mgf-for-agentic-ai.pdf) (Singapore, 20 May 2026). [Article 14 runtime: why oversight of agentic AI has to be evidenced as action, not model](https://futurium.ec.europa.eu/ga/apply-ai-alliance/community-content/article-14-runtime-why-oversight-agentic-ai-has-be-evidenced-action-not-model) is the position post on the EU Apply AI Alliance Futurium. > Vaara helps deployers assemble evidence for their own conformity work. It does not certify compliance or constitute legal advice. Deployers own their obligations under the EU AI Act and other applicable law. ## License -[LICENSE](LICENSE) +Apache 2.0. See [LICENSE](LICENSE).