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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

## [Unreleased]

## [0.39.1] - 2026-05-27

**Theme: SEP-2787 reference impl follows the spec into camelCase.**
soup-oss adopted MCP camelCase convention for SEP-2787 envelope field
names in `modelcontextprotocol/modelcontextprotocol@48c739b1`. Vaara's
proposed-shape reference implementation now emits camelCase JSON keys
on the serialisation boundary while keeping Python-side attributes in
snake_case.

### Changed
- `Attestation.to_dict()` and the JCS-canonical signing payload emit
`plannerDeclared`, `issuerAsserted`, `payloadDerived`, `toolCalls`,
`serverFingerprint`, `secretVersion`, `expSeconds`,
`requestedCapability`, `projectionDigest`. The proposed-shape
reference now matches MCP camelCase convention; Python dataclass
attributes stay snake_case so user code is unchanged.
- `docs/sep2787-overt-mapping.md` field-by-field table updated to the
camelCase shape.
- New `issuer_to_dict` helper in `vaara.attestation._sep2787_types`
replaces the prior `asdict()` call so the issuer block sorts and
renames deterministically without leaking Python-internal names.

### SEP-2787 reference implementation tag
- `sep2787-ref-v1`: camelCase envelope. Pinned for cross-repo
provenance citation against
`modelcontextprotocol/modelcontextprotocol#2787` and the v0 test
vector PR (`#2789`, head `2a9360f`).
- `sep2787-ref-v0` (preserved at commit `3d7af54`): snake_case envelope,
the historical proposed-shape artefact.

## [0.39.0] - 2026-05-27

**Theme: v9 classifier retrain on a BIPIA-augmented corpus, with one
Expand Down
2 changes: 1 addition & 1 deletion clients/ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vaara/client",
"version": "0.39.0",
"version": "0.39.1",
"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",
Expand Down
18 changes: 9 additions & 9 deletions docs/sep2787-overt-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@ reason.
## Trust-surface alignment

The SEP-2787 proposed shape groups envelope fields under three
trust-surface blocks (`planner_declared`, `issuer_asserted`,
`payload_derived`). OVERT does not group fields explicitly but applies
trust-surface blocks (`plannerDeclared`, `issuerAsserted`,
`payloadDerived`). OVERT does not group fields explicitly but applies
the same separation through field semantics. The mapping below names
which block each OVERT field corresponds to.

## Field-by-field mapping

| OVERT Base Envelope (CBOR) | SEP-2787 proposed shape (JSON) | Trust surface | Notes |
|---|---|---|---|
| `blinded_identifier` (32 bytes) | `issuer_asserted.nonce` (base64url) | issuer-asserted | Both serve replay-protection. OVERT uses 32 bytes per action; SEP-2787 uses 18 bytes base64url-encoded per attestation. |
| `request_commitment` (HMAC-SHA256, bytes) | `payload_derived.args_digest` (`sha256:<hex>`) | payload-derived | Direct semantic match. OVERT uses keyed HMAC over a SHA-256 digest of the request content; SEP-2787 ArgsDigest is a plain SHA-256 over JCS-canonical bytes. Both keep the raw payload local. |
| `blinded_identifier` (32 bytes) | `issuerAsserted.nonce` (base64url) | issuer-asserted | Both serve replay-protection. OVERT uses 32 bytes per action; SEP-2787 uses 18 bytes base64url-encoded per attestation. |
| `request_commitment` (HMAC-SHA256, bytes) | `payloadDerived.argsDigest` (`sha256:<hex>`) | payload-derived | Direct semantic match. OVERT uses keyed HMAC over a SHA-256 digest of the request content; SEP-2787 ArgsDigest is a plain SHA-256 over JCS-canonical bytes. Both keep the raw payload local. |
| `encoder_binary_identity` (SHA-256, bytes) | (no direct equivalent) | n/a | OVERT pins the arbiter implementation + version + policy hash at signing time. SEP-2787 v1 has no equivalent. Vaara's reference implementation could attach an extension field for this. |
| `non_content_metadata` (CBOR map) | `planner_declared.requested_capability` (string) | planner-declared | Partial match. OVERT carries structural classification fields (action class, severity, decision); SEP-2787 has only the capability string in the proposed shape. |
| `non_content_metadata` (CBOR map) | `plannerDeclared.requestedCapability` (string) | planner-declared | Partial match. OVERT carries structural classification fields (action class, severity, decision); SEP-2787 has only the capability string in the proposed shape. |
| `monotonic_counter` (uint64) | (no direct equivalent) | n/a | OVERT requires a strictly increasing per-arbiter sequence to detect gaps. SEP-2787 has no monotonic counter; replay protection is per-nonce within TTL. |
| `nanosecond_timestamp` (uint64) | `issuer_asserted.iat` (ISO 8601 string) | issuer-asserted | Same semantics, different encoding. OVERT uses uint64 nanoseconds for cross-language stability; SEP-2787 uses ISO 8601 strings for JSON-native consumption. |
| `key_identifier` (SHA-256 over public key, bytes) | `issuer_asserted.secret_version` (string) | issuer-asserted | OVERT binds the verifying key cryptographically via SHA-256 fingerprint; SEP-2787 names it via opaque version string and leaves key lookup to the verifier. |
| `nanosecond_timestamp` (uint64) | `issuerAsserted.iat` (ISO 8601 string) | issuer-asserted | Same semantics, different encoding. OVERT uses uint64 nanoseconds for cross-language stability; SEP-2787 uses ISO 8601 strings for JSON-native consumption. |
| `key_identifier` (SHA-256 over public key, bytes) | `issuerAsserted.secretVersion` (string) | issuer-asserted | OVERT binds the verifying key cryptographically via SHA-256 fingerprint; SEP-2787 names it via opaque version string and leaves key lookup to the verifier. |
| `arbiter_instance_identifier` (16 bytes, UUID) | (no direct equivalent) | n/a | OVERT identifies the specific arbiter instance that produced the envelope. SEP-2787 conflates this with `iss`. |
| `signature` (Ed25519, 64 bytes) | `signature` (hex-encoded) | binding output | Different curve choices. OVERT mandates Ed25519. SEP-2787 supports HS256 (HMAC-SHA256), ES256 (ECDSA P-256), RS256 (RSASSA-PKCS1-v1_5). |
| (no equivalent) | `planner_declared.intent` (string) | planner-declared | Human-readable justification required by EU AI Act Article 12 audit reconstruction. OVERT can carry this in `non_content_metadata` but does not require it as a top-level field. |
| (no equivalent) | `planner_declared.tool_calls[*].name` and `.server_fingerprint` | planner-declared | MCP-specific binding. OVERT is transport-agnostic and does not name a tool or server in the envelope. |
| (no equivalent) | `plannerDeclared.intent` (string) | planner-declared | Human-readable justification required by EU AI Act Article 12 audit reconstruction. OVERT can carry this in `non_content_metadata` but does not require it as a top-level field. |
| (no equivalent) | `plannerDeclared.toolCalls[*].name` and `.serverFingerprint` | planner-declared | MCP-specific binding. OVERT is transport-agnostic and does not name a tool or server in the envelope. |

## Canonicalization

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "vaara"
version = "0.39.0"
version = "0.39.1"
description = "Adaptive AI Agent Execution Layer for risk scoring, audit trails, and regulatory compliance"
requires-python = ">=3.10"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion src/vaara/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
oversight.
"""

__version__ = "0.39.0"
__version__ = "0.39.1"

from vaara.pipeline import InterceptionPipeline, InterceptionResult

Expand Down
12 changes: 6 additions & 6 deletions src/vaara/attestation/_sep2787_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from __future__ import annotations

import time
from dataclasses import asdict
from typing import Any, Optional

from vaara.attestation._sep2787_canonical import (
Expand All @@ -32,6 +31,7 @@
IssuerAsserted,
PlannerDeclared,
args_to_dict,
issuer_to_dict,
planner_to_dict,
)

Expand All @@ -52,9 +52,9 @@ def _signing_payload(
body = {
"version": version,
"alg": alg,
"planner_declared": planner_to_dict(planner_declared),
"issuer_asserted": asdict(issuer_asserted),
"payload_derived": [args_to_dict(a) for a in payload_derived],
"plannerDeclared": planner_to_dict(planner_declared),
"issuerAsserted": issuer_to_dict(issuer_asserted),
"payloadDerived": [args_to_dict(a) for a in payload_derived],
}
return canonical_json(body)

Expand Down Expand Up @@ -85,10 +85,10 @@ def emit_attestation(
if alg not in VALID_ALGS:
raise AttestationError(f"unsupported alg: {alg!r}")
if not planner_declared.intent.strip():
raise AttestationError("planner_declared.intent MUST be non-empty")
raise AttestationError("plannerDeclared.intent MUST be non-empty")
if not planner_declared.tool_calls:
raise AttestationError(
"planner_declared.tool_calls MUST contain at least one entry"
"plannerDeclared.toolCalls MUST contain at least one entry"
)

issuer_asserted = IssuerAsserted(
Expand Down
32 changes: 22 additions & 10 deletions src/vaara/attestation/_sep2787_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from dataclasses import asdict, dataclass, field
from dataclasses import dataclass, field
from typing import Any, Literal, Optional, Union

Algorithm = Literal["HS256", "ES256", "RS256"]
Expand Down Expand Up @@ -107,8 +107,8 @@ class Attestation:

Composed of three trust-surface blocks plus the signature. The
signature is computed over the JCS-canonical encoding of
``{version, alg, planner_declared, issuer_asserted,
payload_derived}`` and does not cover itself.
``{version, alg, plannerDeclared, issuerAsserted,
payloadDerived}`` and does not cover itself.

The ``payload_derived`` tuple parallels
``planner_declared.tool_calls`` in order: index N here is the args
Expand All @@ -126,9 +126,9 @@ def to_dict(self) -> dict[str, Any]:
return {
"version": self.version,
"alg": self.alg,
"planner_declared": planner_to_dict(self.planner_declared),
"issuer_asserted": asdict(self.issuer_asserted),
"payload_derived": [args_to_dict(a) for a in self.payload_derived],
"plannerDeclared": planner_to_dict(self.planner_declared),
"issuerAsserted": issuer_to_dict(self.issuer_asserted),
"payloadDerived": [args_to_dict(a) for a in self.payload_derived],
"signature": self.signature,
}

Expand All @@ -151,23 +151,35 @@ def args_to_dict(args: ArgsCommitment) -> dict[str, Any]:
return {
"kind": "projection",
"projection": args.projection,
"projection_digest": args.projection_digest,
"projectionDigest": args.projection_digest,
}
raise AttestationError(f"unknown args commitment kind: {type(args)!r}")


def planner_to_dict(planner: PlannerDeclared) -> dict[str, Any]:
out: dict[str, Any] = {
"intent": planner.intent,
"tool_calls": [
"toolCalls": [
{
"name": tc.name,
"server_fingerprint": tc.server_fingerprint,
"serverFingerprint": tc.server_fingerprint,
"args": args_to_dict(tc.args),
}
for tc in planner.tool_calls
],
}
if planner.requested_capability is not None:
out["requested_capability"] = planner.requested_capability
out["requestedCapability"] = planner.requested_capability
return out


def issuer_to_dict(issuer: IssuerAsserted) -> dict[str, Any]:
return {
"alg": issuer.alg,
"expSeconds": issuer.exp_seconds,
"iat": issuer.iat,
"iss": issuer.iss,
"nonce": issuer.nonce,
"secretVersion": issuer.secret_version,
"sub": issuer.sub,
}
6 changes: 3 additions & 3 deletions src/vaara/attestation/_sep2787_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

Implements step 5 of the verification rules in the SEP-2787 draft:

If the toolCalls entry uses args_ref, resolve the URI, compute
If the toolCalls entry uses argsRef, resolve the URI, compute
SHA-256 over the fetched content, and compare against the stored
digest. Confirm the resolved content corresponds to the arguments
being executed. If the entry uses args_projection, compare it
being executed. If the entry uses argsProjection, compare it
against the canonicalized runtime arguments (RFC 8785). Identity
projections MUST match exactly; redacted projections are verified
only to be signed -- the verifier makes no claim about
Expand All @@ -16,7 +16,7 @@
reject with args_commitment_mismatch.

Vaara's three-way args shape (ArgsDigest / ArgsRef / ArgsProjection)
extends the spec's two-way (args_ref / args_projection) with a
extends the spec's two-way (argsRef / argsProjection) with a
commitment-only ArgsDigest where the payload never crosses the
verifier. For ArgsDigest the verifier recomputes the JCS-canonical
hash of the runtime arguments and compares against the bound digest.
Expand Down
8 changes: 4 additions & 4 deletions src/vaara/attestation/sep2787.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
written:

1. **Fact-source labels.** Envelope fields are grouped under three
named blocks by trust surface: ``planner_declared`` (intent, tool
named blocks by trust surface: ``plannerDeclared`` (intent, tool
name and server bindings the agent planner claims),
``issuer_asserted`` (iss, sub, iat, exp, nonce, secret_version,
``issuerAsserted`` (iss, sub, iat, expSeconds, nonce, secretVersion,
alg, set by the attestation issuer at signing time), and
``payload_derived`` (args_digest / args_ref / args_projection,
``payloadDerived`` (argsDigest / argsRef / argsProjection,
deterministically derived from the request payload). The signature
is the binding output and lives at the envelope root.
2. **Three-way args shape.** The v1 draft overloads a single
Expand All @@ -30,7 +30,7 @@
Signing modes follow the v1 draft: HS256 (HMAC-SHA256), ES256 (ECDSA
P-256 raw r||s, not DER), RS256 (RSASSA-PKCS1-v1_5). The signature is
computed over the JCS-canonical encoding of the four envelope blocks
``{version, alg, planner_declared, issuer_asserted, payload_derived}``
``{version, alg, plannerDeclared, issuerAsserted, payloadDerived}``
and is excluded from its own input.

Install: ``pip install 'vaara[attestation]'``. Requires ``rfc8785`` for
Expand Down
12 changes: 8 additions & 4 deletions tests/test_attestation_sep2787.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,15 @@ def test_to_dict_round_trip_shape():
env = _emit_attestation()
d = env.to_dict()
assert set(d) == {
"version", "alg", "planner_declared",
"issuer_asserted", "payload_derived", "signature",
"version", "alg", "plannerDeclared",
"issuerAsserted", "payloadDerived", "signature",
}
assert d["planner_declared"]["tool_calls"][0]["args"]["kind"] == "digest"
assert d["payload_derived"][0]["kind"] == "digest"
assert d["plannerDeclared"]["toolCalls"][0]["args"]["kind"] == "digest"
assert d["plannerDeclared"]["toolCalls"][0]["serverFingerprint"].startswith("sha256:")
assert d["plannerDeclared"]["requestedCapability"] == "filesystem.delete"
assert d["issuerAsserted"]["expSeconds"] == 300
assert d["issuerAsserted"]["secretVersion"] == "v1"
assert d["payloadDerived"][0]["kind"] == "digest"


def test_cross_alg_verification_fails():
Expand Down