Skip to content

feat: v0.13.0 hot policy reload, OVERT Phase 3 IAP, named detectors, HTML dashboard#82

Merged
vaaraio merged 1 commit into
mainfrom
feat/hot-policy-reload
May 16, 2026
Merged

feat: v0.13.0 hot policy reload, OVERT Phase 3 IAP, named detectors, HTML dashboard#82
vaaraio merged 1 commit into
mainfrom
feat/hot-policy-reload

Conversation

@vaaraio
Copy link
Copy Markdown
Owner

@vaaraio vaaraio commented May 16, 2026

Summary

Theme: operator surface + OVERT Phase 3 path. Four additions that close the most legible competitive gaps without diluting the kernel position.

  • B1 Hot policy reload. PolicyController + AdaptiveScorer.apply_policy; POST /v1/policy/reload and vaara policy reload POLICY_PATH. Atomic swap under the scorer's RLock so in-flight evaluate() calls never see a torn (allow, deny) pair. Conformal calibration, MWU experts, and agent profiles preserved across reloads.
  • OVERT 1.0 Phase 3 IAP reference. vaara.attestation.iap.Phase3Attestation wraps a Vaara BaseEnvelope with a notary Ed25519 signature + transparency-log inclusion proof. Structural independence (notary key ≠ Arbiter key) enforced at emit and verify. InProcessTransparencyLog is RFC 6962-style; production deployments can swap in sigstore Rekor at the same call sites.
  • Named injection + PII detector aliases. vaara.detect.detect_injection routes through the AdversarialClassifier behind vaara-bench-v1's numbers. vaara.detect.detect_pii covers email, phone, US SSN, IPv4, credit_card (Luhn-checked), IBAN (mod-97). POST /v1/detect/{injection,pii} and vaara detect {injection,pii} mirror each other and exit non-zero on detection for CI gates.
  • Static HTML article-coverage dashboard. vaara.compliance.dashboard.render_html produces a single self-contained HTML page (embedded CSS, no JS, no external assets, no network calls). vaara compliance dashboard --db PATH --out PATH writes it.

OpenAPI spec, CHANGELOG, COMPLIANCE.md (TOOL-1.4, TOOL-5, OVERT-position section), and README updated.

Test plan

  • 586 tests pass, 12 skipped (53 new tests across 5 files).
  • vaara serve --policy PATH + vaara policy reload POLICY_PATH round-trip.
  • OVERT Phase 3 emit + verify against in-process Merkle log including unbalanced 11-leaf tree.
  • vaara detect injection --stdin exits 1 on injection patterns, 0 on clean text.
  • vaara compliance dashboard --out site/ renders a print-friendly HTML with no remote URLs.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Hot policy reload via HTTP endpoint and CLI command
    • Named injection and PII detection endpoints with CLI parity
    • Phase 3 attestation with transparency-log integration
    • Static HTML compliance dashboard renderer
  • Documentation

    • OpenAPI specification updated with new endpoint definitions
    • CHANGELOG and compliance guidance updated for v0.13.0

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@vaaraio has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 36 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0effabf8-d943-4a01-8d68-bd1027789cc6

📥 Commits

Reviewing files that changed from the base of the PR and between 944a3e6 and f20cc7a.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • COMPLIANCE.md
  • README.md
  • docs/openapi.yaml
  • pyproject.toml
  • src/vaara/__init__.py
  • src/vaara/attestation/__init__.py
  • src/vaara/attestation/iap.py
  • src/vaara/attestation/transparency_log.py
  • src/vaara/cli.py
  • src/vaara/compliance/dashboard.py
  • src/vaara/detect/__init__.py
  • src/vaara/detect/injection.py
  • src/vaara/detect/pii.py
  • src/vaara/policy/__init__.py
  • src/vaara/policy/controller.py
  • src/vaara/scorer/adaptive.py
  • src/vaara/server/app.py
  • src/vaara/server/routes.py
  • src/vaara/server/schemas.py
  • src/vaara/server/state.py
  • tests/test_attestation_iap.py
  • tests/test_compliance_dashboard.py
  • tests/test_detect.py
  • tests/test_policy_controller.py
  • tests/test_policy_reload_http.py
📝 Walkthrough

Walkthrough

Release v0.13.0 adds OVERT Phase 3 reference attestation provider with in-process transparency log, named injection/PII detectors, hot policy reload with atomic semantics, HTTP endpoints for detection and reload, compliance dashboard HTML renderer, and CLI commands for operator tooling. Package version increments to 0.13.0.

Changes

Vaara v0.13.0 Release

Layer / File(s) Summary
Version bump and release documentation
pyproject.toml, src/vaara/__init__.py, CHANGELOG.md, README.md, COMPLIANCE.md
Package version 0.12.0 → 0.13.0; CHANGELOG documents new features and test counts; README and COMPLIANCE.md updated with v0.13.0 reference IAP details and Phase 3 attestation mapping.
OVERT Phase 3 attestation provider and transparency log
src/vaara/attestation/transparency_log.py, src/vaara/attestation/iap.py, src/vaara/attestation/__init__.py, tests/test_attestation_iap.py
RFC 6962 Merkle transparency log with append/proof/root; Phase3Attestation dataclass; emit/verify functions with canonical CBOR encoding, notary signing, and structural independence enforcement; 11 test cases covering round-trip, negative cases, and log errors.
Prompt injection and PII detection
src/vaara/detect/injection.py, src/vaara/detect/pii.py, src/vaara/detect/__init__.py, tests/test_detect.py
detect_injection with ML backend fallback and regex heuristic; detect_pii with regex patterns for six categories plus Luhn/IBAN checksum validation; InjectionResult and PIIResult dataclasses; public re-export via vaara.detect; unit and HTTP contract tests.
PolicyController with atomic hot-reload
src/vaara/policy/controller.py, src/vaara/policy/__init__.py, src/vaara/scorer/adaptive.py, tests/test_policy_controller.py
PolicyController manages policy state with monotonic version and listener registry; reload atomically swaps policy and notifies listeners under re-entrant lock; AdaptiveScorer.apply_policy rebinds thresholds and clears sequence cache; 7 test cases covering listener registration, reloads, failure modes, and concurrency.
OpenAPI spec and HTTP wire schemas
docs/openapi.yaml, src/vaara/server/schemas.py
Three new POST endpoints documented: /v1/detect/injection, /v1/detect/pii, /v1/policy/reload with request/response schemas; Pydantic models enforce extra="forbid" and validate ranges/lengths.
Server routes, state, and app integration
src/vaara/server/routes.py, src/vaara/server/app.py, src/vaara/server/state.py, tests/test_policy_reload_http.py
Routes call detector functions and controller.reload, validate exactly-one-of path/body for policy reload; ServerState stores optional policy_controller and registers scorer listener; create_app gates reload endpoint on controller presence; 7 HTTP contract tests.
Compliance dashboard HTML renderer
src/vaara/compliance/dashboard.py, tests/test_compliance_dashboard.py
render_html assembles self-contained HTML page: system metadata, audit status, per-domain article tables with status pills, detailed cards with gaps/recommendations; embedded CSS, no external assets, print-friendly styling; 9 test cases covering structure, escaping, and self-containment.
CLI: detection, reload, dashboard, and policy loading
src/vaara/cli.py
New subcommands: detect injection/pii (text input modes, JSON output, exit code signals), compliance dashboard (renders/writes HTML), policy reload (HTTP POST to server); serve --policy loads policy via PolicyController at startup; shared input helpers and format override support.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vaaraio/vaara#75: Prior PR introduced the HTTP server foundation (FastAPI routes and ServerState wiring) that this PR extends with new endpoints for detection and policy reload.

Poem

A rabbit hops through cryptographic fields,
Where attestations notarize their seals.
Phase 3 transparency blooms in sight,
While detectors sniff for shadows in the light. 🐰✨
Hot policies reload without a care,
And dashboards render truth, laid bare.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the four main features added in this release: hot policy reload, OVERT Phase 3 IAP (Independent Attestation Provider), named detectors, and HTML dashboard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hot-policy-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/vaara/cli.py
print(f"vaara policy reload: HTTP {exc.code}", file=sys.stderr)
try:
print(exc.read().decode("utf-8"), file=sys.stderr)
except Exception:
@vaaraio vaaraio force-pushed the feat/hot-policy-reload branch from 944a3e6 to 101afb4 Compare May 16, 2026 23:49
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (4)
src/vaara/server/schemas.py (2)

208-213: ⚡ Quick win

Add bounds to PolicyReloadResponse primitives.

version and counts should be non-negative/monotonic at the schema layer, consistent with the API contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/server/schemas.py` around lines 208 - 213, The PolicyReloadResponse
schema allows negative values for version and count fields; update class
PolicyReloadResponse so that version, sequence_count, action_class_count, and
escalation_route_count are constrained to be non-negative (e.g., use pydantic
Field(..., ge=0) or an appropriate non-negative int type) to enforce the API
contract at the schema layer; modify the field declarations for version,
sequence_count, action_class_count, and escalation_route_count in
PolicyReloadResponse accordingly.

173-192: ⚡ Quick win

Tighten response enums and numeric bounds to match the documented contract.

The current models accept broader values than the OpenAPI contract (backend, category, offset, length), which can hide regressions.

✅ Suggested tightening
 class DetectInjectionResponse(BaseModel):
     detected: bool
     score: float = Field(ge=0, le=1)
     threshold: float = Field(ge=0, le=1)
     bundle_version: str
-    backend: str
+    backend: Literal["vaara_adversarial", "heuristic"]

 class DetectPIIFinding(BaseModel):
-    category: str
+    category: Literal["email", "phone", "ssn", "ipv4", "credit_card", "iban"]
     value: str
-    offset: int
-    length: int
+    offset: int = Field(ge=0)
+    length: int = Field(ge=1)
tests/test_policy_reload_http.py (1)

94-98: ⚡ Quick win

Assert the error code for the “neither path nor body” branch.

This test currently checks only HTTP status; asserting error.code would lock the API contract like the sibling negative tests do.

🧪 Suggested assertion
 def test_reload_rejects_neither_path_nor_body(client_with_controller):
     client, _ = client_with_controller
     r = client.post("/v1/policy/reload", json={})
     assert r.status_code == 400
+    assert r.json()["error"]["code"] == "invalid_request"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_policy_reload_http.py` around lines 94 - 98, Update
test_reload_rejects_neither_path_nor_body to assert the JSON error code in
addition to the HTTP 400: after calling client.post("/v1/policy/reload",
json={}) parse r.json(), ensure there is an "error" object and assert its "code"
equals the same error code used by the sibling negative tests for the "neither
path nor body" branch (match the value used in those tests), referencing the
test function name test_reload_rejects_neither_path_nor_body and the POST to
"/v1/policy/reload" to locate where to add the assertion.
docs/openapi.yaml (1)

561-577: ⚡ Quick win

Encode path/body XOR in schema (not only description text).

Right now the constraint is prose-only; schema-based clients can’t enforce it preflight.

📌 Proposed spec patch
     PolicyReloadRequest:
       type: object
       description: |
         Exactly one of `path` or `body` must be supplied. `path` lets the
         server read the policy file directly; `body` carries an inline
         parsed document for cases where the operator runs on a different
         host than the server.
+      oneOf:
+        - required: [path]
+        - required: [body]
       properties:
         path: { type: string, maxLength: 4096 }
         body:
           type: object
           additionalProperties: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/openapi.yaml` around lines 561 - 577, PolicyReloadRequest currently
enforces the "exactly one of path or body" rule only in prose; update the schema
to express this using oneOf so tools can validate it: replace the flat
properties block with a oneOf containing two subschemas — one that requires
"path" (and defines path: {type: string, maxLength: 4096} and allows "format")
and one that requires "body" (and defines body: {type: object,
additionalProperties: true} and allows "format"); keep the existing "format"
enum/nullable and the original path/body property schemas (including maxLength
and additionalProperties) inside those subschemas so validators enforce the XOR
constraint for PolicyReloadRequest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 9-73: The CHANGELOG entry header "## [0.13.0] - 2026-05-17" has
the wrong date; verify whether the release was actually on 2026-05-17 or still
pending and update the header in CHANGELOG.md accordingly (either set the
correct release date or use an "Unreleased"/date-to-be-filled marker), making
sure the version header "## [0.13.0] - 2026-05-17" is edited to the confirmed
value.

In `@docs/openapi.yaml`:
- Line 171: Top-level OpenAPI tags are missing entries for the tags used
elsewhere; add global tag declarations for "detect" and "policy" in the
document's top-level tags array so generated docs can show their descriptions
and grouping metadata. Specifically, create entries named detect and policy
(e.g., tags: - name: detect description: "..." and - name: policy description:
"...") with appropriate descriptions (and optional externalDocs) so the API
operations that reference the detect and policy tags pick up their metadata.

In `@src/vaara/attestation/iap.py`:
- Around line 147-149: The append followed by inclusion_proof is non-atomic
(transparency_log.append(envelope_cbor) then
transparency_log.inclusion_proof(log_entry.log_index)), so concurrent appends
can produce a proof from a different tree snapshot; implement a single atomic
operation on the log (e.g., add a new method
InProcessTransparencyLog.append_with_inclusion_proof(envelope_cbor) that is
executed under the same lock) that returns both LogEntry and InclusionProof
together, then replace both call sites (the current append + inclusion_proof
pairs around envelope_cbor/log_entry.log_index, including the similar block at
lines 163-166) to call this new append_with_inclusion_proof so log_entry and
proof are derived from the same snapshot.
- Around line 230-245: The structural-independence check that ensures the
envelope's "key_identifier" differs from attestation.notary_key_identifier must
run regardless of whether arbiter_public_key_raw is provided: move the decoded
cbor parsing and the comparisons using decoded.get("key_identifier") and
_sha256(arbiter_public_key_raw) (and the check against
attestation.notary_key_identifier) out of the arbiter_public_key_raw-only branch
so the notary-vs-arbiter collision check is unconditional; keep only the arbiter
signature-specific verification (e.g., importing cbor2 solely for signature
verification and any signature validation logic that depends on
arbiter_public_key_raw) inside the conditional so callers can omit arbiter
signature verification but cannot bypass the structural independence check.

In `@src/vaara/compliance/dashboard.py`:
- Line 91: Remove the unnecessary f-string prefix on the HTML literal causing
Ruff F541: replace the f'<div class="card">' occurrence in
src/vaara/compliance/dashboard.py with a normal string literal '<div
class="card">'. Locate the same change wherever the identical f-prefixed static
HTML fragment appears (e.g., inside the HTML-building block or function that
constructs the card markup) to eliminate the redundant f-string usage and
unblock CI.

In `@src/vaara/detect/injection.py`:
- Line 106: The code currently coerces threshold to float (th =
_DEFAULT_THRESHOLD if threshold is None else float(threshold)) but does not
validate it; update the handling of the input parameter `threshold` (and
resulting local `th`) to explicitly check that the value is finite and within
[0, 1], using math.isfinite after conversion and raising a ValueError with a
clear message if the check fails; keep the None fallback to _DEFAULT_THRESHOLD
and perform these checks on the final `th` before it is used for scoring
(reference the symbols `threshold` and `th` in injection.py).

In `@src/vaara/policy/controller.py`:
- Around line 67-70: The current code appends a listener to self._listeners
before calling listener(self._policy), which can leave a broken listener
registered if its initial apply raises; change the logic in the method in
controller.py so you first invoke listener(self._policy) and only if that call
succeeds append the listener to self._listeners (use self._lock around the
append to keep thread-safety); if the initial call raises, do not append the
listener (either re-raise or log/handle the exception as appropriate).
- Around line 102-105: The code path handling fmt currently only checks for
"yaml" and "json" and silently falls through to auto-detection for any other
value; update the reload handler (the branch that examines the fmt variable in
src/vaara/policy/controller.py) to explicitly reject unsupported formats by
raising a clear exception (e.g., ValueError) when fmt is not "json" or "yaml"
instead of falling back, and include the invalid fmt value in the error message;
keep the existing calls to from_yaml(source) and from_json(source) for the
supported cases.

In `@src/vaara/scorer/adaptive.py`:
- Around line 687-700: The apply_policy method currently annotates its parameter
as the string literal "Policy"; remove the unnecessary quotes and use the real
type name Policy in the signature (def apply_policy(self, policy: Policy) ->
None) since the module already conditionally imports Policy from
vaara.policy.schema; update the annotation only (no other logic changes) and
keep the local import/check and isinstance(policy, Policy) validation intact.

In `@tests/test_detect.py`:
- Around line 123-129: The test currently catches ImportError across both
imports which can hide real failures; update the try/except so only the optional
dependency import for TestClient is caught and handled (e.g., try: from
fastapi.testclient import TestClient except ImportError: TestClient = None)
while importing create_app from vaara.server stays outside that except so any
ImportError from create_app propagates and fails the tests; reference TestClient
and create_app in tests/test_detect.py to locate and adjust the import blocks
accordingly.

---

Nitpick comments:
In `@docs/openapi.yaml`:
- Around line 561-577: PolicyReloadRequest currently enforces the "exactly one
of path or body" rule only in prose; update the schema to express this using
oneOf so tools can validate it: replace the flat properties block with a oneOf
containing two subschemas — one that requires "path" (and defines path: {type:
string, maxLength: 4096} and allows "format") and one that requires "body" (and
defines body: {type: object, additionalProperties: true} and allows "format");
keep the existing "format" enum/nullable and the original path/body property
schemas (including maxLength and additionalProperties) inside those subschemas
so validators enforce the XOR constraint for PolicyReloadRequest.

In `@src/vaara/server/schemas.py`:
- Around line 208-213: The PolicyReloadResponse schema allows negative values
for version and count fields; update class PolicyReloadResponse so that version,
sequence_count, action_class_count, and escalation_route_count are constrained
to be non-negative (e.g., use pydantic Field(..., ge=0) or an appropriate
non-negative int type) to enforce the API contract at the schema layer; modify
the field declarations for version, sequence_count, action_class_count, and
escalation_route_count in PolicyReloadResponse accordingly.

In `@tests/test_policy_reload_http.py`:
- Around line 94-98: Update test_reload_rejects_neither_path_nor_body to assert
the JSON error code in addition to the HTTP 400: after calling
client.post("/v1/policy/reload", json={}) parse r.json(), ensure there is an
"error" object and assert its "code" equals the same error code used by the
sibling negative tests for the "neither path nor body" branch (match the value
used in those tests), referencing the test function name
test_reload_rejects_neither_path_nor_body and the POST to "/v1/policy/reload" to
locate where to add the assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff522a8a-d9e8-4f24-b8d7-6b50e412f666

📥 Commits

Reviewing files that changed from the base of the PR and between 5d38baa and 944a3e6.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • COMPLIANCE.md
  • README.md
  • docs/openapi.yaml
  • pyproject.toml
  • src/vaara/__init__.py
  • src/vaara/attestation/__init__.py
  • src/vaara/attestation/iap.py
  • src/vaara/attestation/transparency_log.py
  • src/vaara/cli.py
  • src/vaara/compliance/dashboard.py
  • src/vaara/detect/__init__.py
  • src/vaara/detect/injection.py
  • src/vaara/detect/pii.py
  • src/vaara/policy/__init__.py
  • src/vaara/policy/controller.py
  • src/vaara/scorer/adaptive.py
  • src/vaara/server/app.py
  • src/vaara/server/routes.py
  • src/vaara/server/schemas.py
  • src/vaara/server/state.py
  • tests/test_attestation_iap.py
  • tests/test_compliance_dashboard.py
  • tests/test_detect.py
  • tests/test_policy_controller.py
  • tests/test_policy_reload_http.py

Comment thread CHANGELOG.md
Comment on lines +9 to +73
## [0.13.0] - 2026-05-17

**Theme: operator surface + OVERT Phase 3 path.** Four additions that
close the most legible competitive gaps without diluting the kernel
position. Hot policy reload meets the Galileo Agent Control selling
point on its own ground. The OVERT 1.0 Phase 3 Independent Attestation
Provider (IAP) reference closes the AAL-3 → AAL-4 promotion path that
v0.11.0's Provisional Receipt opens, so Vaara owns the full path
without forcing dependence on an external IAP vendor. Named injection
and PII detectors expose existing scoring surface under buyer-visible
labels. A static HTML article-coverage dashboard adds the auditor-
facing visual artefact that the peer set has converged on.

### Added
- **Hot policy reload.** New `vaara.policy.controller.PolicyController`
owns the live `Policy` and runs registered listeners under a write
lock on `reload()`. `AdaptiveScorer.apply_policy(policy)` rebinds
thresholds and sequence patterns atomically under the scorer's own
RLock; an `evaluate()` call in flight on another thread either sees
the old `(allow, deny)` pair or the new one, never a torn half.
Conformal calibration, MWU expert state, and agent profiles are
preserved across reloads. Malformed reloads are rejected with the
previous policy left live. `POST /v1/policy/reload` accepts a
server-side path or an inline body; `vaara serve --policy PATH`
enables the endpoint; `vaara policy reload POLICY_PATH` triggers
reload over HTTP from the operator's shell.
- **OVERT 1.0 AAL-4 Phase 3 IAP reference.** New
`vaara.attestation.iap` ships a `Phase3Attestation` dataclass that
wraps a Vaara `BaseEnvelope` with a notary Ed25519 signature (over a
domain-separated prefix + canonical-CBOR of the inner envelope
including its signature) and a transparency-log inclusion proof.
Structural independence between the Arbiter key and the notary key
is enforced at both emit and verify. New
`vaara.attestation.transparency_log.InProcessTransparencyLog`
implements an RFC 6962-style binary Merkle tree with domain-
separated leaf and internal hashes; `append()` /
`inclusion_proof()` / `root_hash` match the shape a sigstore Rekor
adapter would expose, so a production deployment can swap in Rekor
at the same call sites without changing the IAP contract.
- **Named injection + PII detector aliases.** `vaara.detect.detect_injection`
routes free text through the same AdversarialClassifier behind
vaara-bench-v1's published numbers (heuristic fallback when the ml
extra is absent; the `backend` field reports which path served the
call). `vaara.detect.detect_pii` is a zero-dependency regex extractor
over six categories — email, phone, US SSN, IPv4, credit_card
(Luhn-checked), IBAN (mod-97 checksum). `POST /v1/detect/injection`
and `POST /v1/detect/pii` mirror the CLI. `vaara detect injection`
and `vaara detect pii` read text from `--text`, `--file`, or
`--stdin` and exit non-zero when the detector fires.
- **Static HTML article-coverage dashboard.**
`vaara.compliance.dashboard.render_html` produces a single
self-contained HTML page with embedded CSS, no JavaScript, no
external assets, no network calls. Same content as the Markdown
renderer (system metadata, audit-trail integrity, summary, critical
gaps, per-domain article tables, detailed per-article sections) with
status badges as colored pills and a print-friendly stylesheet.
`vaara compliance dashboard --db PATH --out PATH` writes the page;
a trailing slash or existing directory drops `index.html` inside.
- **OpenAPI spec coverage.** `docs/openapi.yaml` adds
`/v1/detect/injection`, `/v1/detect/pii`, and `/v1/policy/reload`
with full request and response schemas. The spec remains the
authoritative integration surface.
- 53 new tests (14 PolicyController + reload HTTP; 11 IAP +
transparency-log; 19 detect (injection + PII + HTTP); 9 HTML
dashboard). Total 586 passing, 12 skipped.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify the release date.

The changelog shows release date 2026-05-17, but according to the PR context the current date is May 16, 2026. If this PR hasn't merged yet, the release date should either match the actual release date or be updated when the release is cut.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 9 - 73, The CHANGELOG entry header "## [0.13.0] -
2026-05-17" has the wrong date; verify whether the release was actually on
2026-05-17 or still pending and update the header in CHANGELOG.md accordingly
(either set the correct release date or use an "Unreleased"/date-to-be-filled
marker), making sure the version header "## [0.13.0] - 2026-05-17" is edited to
the confirmed value.

Comment thread docs/openapi.yaml

/v1/detect/injection:
post:
tags: [detect]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and get its size
ls -lh docs/openapi.yaml && wc -l docs/openapi.yaml

Repository: vaaraio/vaara

Length of output: 145


🏁 Script executed:

# Read the global tags section (around lines 30-32)
sed -n '25,45p' docs/openapi.yaml

Repository: vaaraio/vaara

Length of output: 778


🏁 Script executed:

# Read line 171 to verify the detect tag usage
sed -n '165,180p' docs/openapi.yaml

Repository: vaaraio/vaara

Length of output: 655


🏁 Script executed:

# Read line 215 to verify the policy tag usage
sed -n '210,220p' docs/openapi.yaml

Repository: vaaraio/vaara

Length of output: 515


🏁 Script executed:

# Find all tags used in operation definitions
rg 'tags:\s*\[' docs/openapi.yaml -o | sort | uniq

Repository: vaaraio/vaara

Length of output: 65


🏁 Script executed:

# Get all tags used in operation definitions with context
rg 'tags:\s*\[(\w+)\]' docs/openapi.yaml -o --replace '$1' | sort | uniq

Repository: vaaraio/vaara

Length of output: 90


🏁 Script executed:

# Verify the exact global tags declaration
sed -n '30,35p' docs/openapi.yaml

Repository: vaaraio/vaara

Length of output: 251


Declare detect and policy in the global OpenAPI tags list.

Lines 171 and 215 reference tags not declared in the top-level catalog (lines 30–32). Without these declarations, generated documentation loses tag descriptions and grouping metadata.

📌 Proposed spec patch
 tags:
   - { name: score, description: Risk evaluation }
   - { name: audit, description: Hash-chained audit trail }
   - { name: server, description: Server identity and liveness }
+  - { name: detect, description: Prompt-injection and PII detection }
+  - { name: policy, description: Runtime policy operations }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/openapi.yaml` at line 171, Top-level OpenAPI tags are missing entries
for the tags used elsewhere; add global tag declarations for "detect" and
"policy" in the document's top-level tags array so generated docs can show their
descriptions and grouping metadata. Specifically, create entries named detect
and policy (e.g., tags: - name: detect description: "..." and - name: policy
description: "...") with appropriate descriptions (and optional externalDocs) so
the API operations that reference the detect and policy tags pick up their
metadata.

Comment on lines +147 to +149
log_entry: LogEntry = transparency_log.append(envelope_cbor)
proof: InclusionProof = transparency_log.inclusion_proof(log_entry.log_index)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make append + inclusion-proof capture atomic.

Line 147 and Line 148 are separate calls, so concurrent appends can produce inclusion_proof_siblings for a newer tree while log_tree_size/log_root_at_append remain at append-time values. That can create internally inconsistent attestations and intermittent verification failures.

Suggested direction
-    log_entry: LogEntry = transparency_log.append(envelope_cbor)
-    proof: InclusionProof = transparency_log.inclusion_proof(log_entry.log_index)
+    log_entry, proof = transparency_log.append_with_inclusion_proof(envelope_cbor)

Implement append_with_inclusion_proof() inside InProcessTransparencyLog under one lock so log_entry and proof are derived from the same tree snapshot.

Also applies to: 163-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/attestation/iap.py` around lines 147 - 149, The append followed by
inclusion_proof is non-atomic (transparency_log.append(envelope_cbor) then
transparency_log.inclusion_proof(log_entry.log_index)), so concurrent appends
can produce a proof from a different tree snapshot; implement a single atomic
operation on the log (e.g., add a new method
InProcessTransparencyLog.append_with_inclusion_proof(envelope_cbor) that is
executed under the same lock) that returns both LogEntry and InclusionProof
together, then replace both call sites (the current append + inclusion_proof
pairs around envelope_cbor/log_entry.log_index, including the similar block at
lines 163-166) to call this new append_with_inclusion_proof so log_entry and
proof are derived from the same snapshot.

Comment on lines +230 to +245
if arbiter_public_key_raw is not None:
try:
import cbor2
except ImportError as exc:
raise EnvelopeError(
"cbor2 not installed. Install with: pip install "
"'vaara[attestation]'"
) from exc

decoded = cbor2.loads(attestation.envelope_cbor)
if not isinstance(decoded, dict):
return False
if _sha256(arbiter_public_key_raw) != decoded.get("key_identifier"):
return False
if decoded.get("key_identifier") == attestation.notary_key_identifier:
return False
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce structural independence even when arbiter signature verification is skipped.

Right now the arbiter/notary key-collision check is gated by arbiter_public_key_raw is not None (Line 230). That allows a caller to bypass the Phase 3 structural-independence validation by omitting the arbiter key. The independence check should be unconditional; only inner arbiter signature verification should remain optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/attestation/iap.py` around lines 230 - 245, The
structural-independence check that ensures the envelope's "key_identifier"
differs from attestation.notary_key_identifier must run regardless of whether
arbiter_public_key_raw is provided: move the decoded cbor parsing and the
comparisons using decoded.get("key_identifier") and
_sha256(arbiter_public_key_raw) (and the check against
attestation.notary_key_identifier) out of the arbiter_public_key_raw-only branch
so the notary-vs-arbiter collision check is unconditional; keep only the arbiter
signature-specific verification (e.g., importing cbor2 solely for signature
verification and any signature validation logic that depends on
arbiter_public_key_raw) inside the conditional so callers can omit arbiter
signature verification but cannot bypass the structural independence check.

Comment thread src/vaara/compliance/dashboard.py Outdated
"""
if not isinstance(text, str):
raise TypeError(f"text must be str, got {type(text).__name__}")
th = _DEFAULT_THRESHOLD if threshold is None else float(threshold)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate threshold bounds before scoring.

At Line 106, threshold is coerced to float but not checked for finiteness or [0, 1] bounds, which can silently produce invalid decisions.

Suggested fix
+import math
 ...
-    th = _DEFAULT_THRESHOLD if threshold is None else float(threshold)
+    th = _DEFAULT_THRESHOLD if threshold is None else float(threshold)
+    if not math.isfinite(th) or not (0.0 <= th <= 1.0):
+        raise ValueError(f"threshold must be finite and in [0, 1], got {th!r}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
th = _DEFAULT_THRESHOLD if threshold is None else float(threshold)
th = _DEFAULT_THRESHOLD if threshold is None else float(threshold)
if not math.isfinite(th) or not (0.0 <= th <= 1.0):
raise ValueError(f"threshold must be finite and in [0, 1], got {th!r}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/detect/injection.py` at line 106, The code currently coerces
threshold to float (th = _DEFAULT_THRESHOLD if threshold is None else
float(threshold)) but does not validate it; update the handling of the input
parameter `threshold` (and resulting local `th`) to explicitly check that the
value is finite and within [0, 1], using math.isfinite after conversion and
raising a ValueError with a clear message if the check fails; keep the None
fallback to _DEFAULT_THRESHOLD and perform these checks on the final `th` before
it is used for scoring (reference the symbols `threshold` and `th` in
injection.py).

Comment thread src/vaara/policy/controller.py
Comment on lines +102 to +105
if fmt == "yaml":
return from_yaml(source)
if fmt == "json":
return from_json(source)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject unsupported format values explicitly.

reload(..., format=...) documents only "json" and "yaml", but unsupported values currently fall through to auto-detection, which is surprising and hard to debug.

Suggested fix
 def _load(source: Union[str, Path, dict], fmt: Optional[str]) -> Policy:
+    if fmt is not None and fmt not in {"json", "yaml"}:
+        raise PolicyError(f"unsupported policy format: {fmt}")
     if isinstance(source, dict):
         return from_dict(source)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/policy/controller.py` around lines 102 - 105, The code path
handling fmt currently only checks for "yaml" and "json" and silently falls
through to auto-detection for any other value; update the reload handler (the
branch that examines the fmt variable in src/vaara/policy/controller.py) to
explicitly reject unsupported formats by raising a clear exception (e.g.,
ValueError) when fmt is not "json" or "yaml" instead of falling back, and
include the invalid fmt value in the error message; keep the existing calls to
from_yaml(source) and from_json(source) for the supported cases.

Comment on lines +687 to +700
def apply_policy(self, policy: "Policy") -> None:
"""Rebind thresholds and sequence patterns from a loaded policy.

Runs under the scorer's RLock so an evaluate() in flight on
another thread either sees the old or the new pair completely.
The conformal calibrator, MWU expert state, agent profiles, and
cross-agent history are intentionally preserved — the operator
intent of a hot reload is "change the policy I'm enforcing,"
not "wipe the model state I've been calibrating against."
"""
from vaara.policy.schema import Policy # local import to avoid cycles
if not isinstance(policy, Policy):
raise TypeError(
f"apply_policy requires a Policy, got {type(policy).__name__}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the failing pattern and absence/presence of type-only import.
set -e
rg -n 'def apply_policy\(self, policy:\s*"Policy"\)' src/vaara/scorer/adaptive.py
rg -n 'TYPE_CHECKING|from vaara\.policy\.schema import Policy' src/vaara/scorer/adaptive.py

Repository: vaaraio/vaara

Length of output: 316


Remove unnecessary string literal quotes from Policy annotation.

Line 690 uses policy: "Policy" as a string literal. Since TYPE_CHECKING and the conditional import from vaara.policy.schema are already present in the module (lines 34–37), the annotation can use the direct type name without quotes.

Change line 690 from:

def apply_policy(self, policy: "Policy") -> None:

to:

def apply_policy(self, policy: Policy) -> None:
🧰 Tools
🪛 GitHub Actions: CI / 0_tests (3.11).txt

[error] 687-687: Ruff (F821): Undefined name Policy.

🪛 GitHub Actions: CI / 1_tests (3.10).txt

[error] 687-687: ruff check . reported F821 Undefined name Policy (type annotation string "Policy" but Policy is not defined/imported).

🪛 GitHub Actions: CI / 2_tests (3.13).txt

[error] 687-687: ruff check: F821 Undefined name Policy.

🪛 GitHub Actions: CI / 4_tests (3.12).txt

[error] 687-687: ruff F821 Undefined name Policy

🪛 GitHub Actions: CI / tests (3.10)

[error] 687-687: ruff check: F821 Undefined name Policy (type name referenced as string annotation but not defined/imported in scope).

🪛 GitHub Actions: CI / tests (3.11)

[error] 687-687: Ruff (F821) Undefined name Policy (type annotation string).

🪛 GitHub Actions: CI / tests (3.12)

[error] 687-687: ruff check failed (F821): Undefined name Policy in type annotation apply_policy(self, policy: "Policy").

🪛 GitHub Actions: CI / tests (3.13)

[error] 687-687: ruff check: F821 Undefined name Policy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vaara/scorer/adaptive.py` around lines 687 - 700, The apply_policy method
currently annotates its parameter as the string literal "Policy"; remove the
unnecessary quotes and use the real type name Policy in the signature (def
apply_policy(self, policy: Policy) -> None) since the module already
conditionally imports Policy from vaara.policy.schema; update the annotation
only (no other logic changes) and keep the local import/check and
isinstance(policy, Policy) validation intact.

Comment thread tests/test_detect.py
…HTML dashboard

Theme: operator surface + OVERT Phase 3 path. Four additions that
close the most legible competitive gaps without diluting the kernel
position.

Hot policy reload. PolicyController owns the live Policy and runs
registered listeners under a write lock on reload. AdaptiveScorer
.apply_policy rebinds thresholds and sequence patterns atomically
under the scorer's own RLock; an evaluate() in flight either sees the
old (allow, deny) pair or the new one, never a torn half. Conformal
calibration, MWU expert state, and agent profiles are preserved
across reloads. Malformed reloads are rejected with the previous
policy left live. POST /v1/policy/reload accepts a server-side path
or an inline body; vaara serve --policy PATH enables the endpoint;
vaara policy reload POLICY_PATH triggers it over HTTP.

OVERT 1.0 AAL-4 Phase 3 IAP reference. vaara.attestation.iap ships a
Phase3Attestation that wraps a Vaara BaseEnvelope with a notary
Ed25519 signature over canonical CBOR of all nine envelope fields
(inner Arbiter signature bound by reference) plus a transparency-log
inclusion proof. Structural independence between the Arbiter key and
the notary key is enforced at both emit and verify.
InProcessTransparencyLog is an RFC 6962-style binary Merkle log with
domain-separated leaf and internal hashes; append, inclusion_proof,
and root_hash match the shape a sigstore Rekor adapter would expose,
so a production deployment can swap in Rekor at the same call sites.

Named injection + PII detector aliases. vaara.detect.detect_injection
routes free text through the same AdversarialClassifier behind
vaara-bench-v1's published numbers (heuristic fallback when the ml
extra is absent; the backend field reports which path served).
vaara.detect.detect_pii is a zero-dependency regex extractor over
email, phone, US SSN, IPv4, credit_card (Luhn-checked), and IBAN
(mod-97 checksum). POST /v1/detect/injection and POST /v1/detect/pii
mirror the CLI. vaara detect injection and vaara detect pii read
text from --text, --file, or --stdin and exit non-zero when the
detector fires.

Static HTML article-coverage dashboard.
vaara.compliance.dashboard.render_html produces a single
self-contained HTML page with embedded CSS, no JavaScript, no
external assets, no network calls. Same content as render_markdown
(system metadata, audit-trail integrity, summary, critical gaps,
per-domain article tables, per-article detail) with status badges as
colored pills and a print-friendly stylesheet. vaara compliance
dashboard --db PATH --out PATH writes the page.

docs/openapi.yaml adds /v1/policy/reload, /v1/detect/injection, and
/v1/detect/pii with full request and response schemas. COMPLIANCE.md
TOOL-1.4 and TOOL-5 rows updated to reflect that Phase 3 is now in
tree; the AAL-4 requirement that the notary keys live with an
independent operator is preserved.

586 tests pass, 12 skipped.
@vaaraio vaaraio force-pushed the feat/hot-policy-reload branch from 101afb4 to f20cc7a Compare May 16, 2026 23:53
import pytest

try:
import cbor2 # noqa: F401
@vaaraio vaaraio merged commit 188ddf5 into main May 16, 2026
10 checks passed
@vaaraio vaaraio deleted the feat/hot-policy-reload branch May 16, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants