release(v0.42.0): execution receipts paired with SEP-2787#167
Conversation
Execution receipts are the post-execution sibling of the SEP-2787 request attestation. SEP-2787 binds a tools/call request before it runs; the receipt covers the deferred half, what happened to it. The two together give end-to-end accountability for one action. The envelope is three blocks (backLink, receiptAsserted, outcomeDerived) plus a signature, reusing the SEP-2787 RFC 8785 canonicalization and the HS256/ES256/RS256 signing stack unchanged. A verifier that already checks SEP-2787 signatures needs no new crypto for receipts. backLink pins the attestation by nonce and by a digest over its full wire bytes. outcomeDerived carries the status, completion time, and an optional result commitment reusing the argument-commitment shapes. A receipt is a durable record rather than a time-bounded capability, so there is no TTL. Ships with v0 conformance vectors (pinned keys, five cases across all three algorithms plus negative back-link and result-mismatch cases) and a stdlib-only independent walker that verifies them without importing Vaara. Library surface only this release; pairing both envelopes into the MCP proxy emission path is the next increment. Also folds a deterministic floor for cloud instance-metadata endpoints into the classifier. The v9 model underweighted a bare http_post to AWS IMDS, the GCP metadata server, and ECS task-role, scoring them below the calibrated threshold. The floor lifts them above it regardless of model output, defense-in-depth on top of the learned score. The match list covers the parser-confusion encodings that slip a literal check too: the IPv6 link-local address AWS serves IMDS on, and the dotless decimal and hex forms of the IMDS IPv4 address, each bounded against a longer digit or hex run. Not exhaustive on purpose: IMDSv2 token flows, DNS rebinding, and arbitrary octal or mixed encodings still fall back to the model. 946 passed, 12 skipped, ruff clean. All version surfaces at 0.42.0.
📝 WalkthroughWalkthroughThis release introduces execution receipts (v0.42.0)—a post-execution complement to SEP-2787 attestations that binds outcomes to attested requests via back-links and signatures—alongside a metadata-SSRF detection floor in the adversarial classifier. The feature includes comprehensive type definitions, emission/verification logic, conformance vectors with an independent checker, and detailed documentation. Version bumps reflect the release across all language clients and server manifests. ChangesExecution Receipts v0 Feature
Adversarial Classifier Metadata SSRF Floor
Release Infrastructure & Documentation
🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
README.md (1)
17-17: ⚡ Quick winTighten compliance claim wording to avoid legal overstatement.
Line 17 says Vaara “covers EU AI Act compliance,” which reads like a guarantee and can conflict with your own disclaimer later. Prefer “supports evidence for EU AI Act compliance” (or equivalent).
Suggested wording tweak
-Vaara is the tamper-evident runtime evidence layer for AI systems. It covers EU AI Act compliance, and any other case where you need to prove what an agent actually did. Open source, no SaaS, no telemetry. +Vaara is the tamper-evident runtime evidence layer for AI systems. It supports EU AI Act compliance evidence, and any other case where you need to prove what an agent actually did. Open source, no SaaS, no telemetry.🤖 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 `@README.md` at line 17, Update the sentence that currently reads "Vaara is the tamper-evident runtime evidence layer for AI systems. It covers EU AI Act compliance, and any other case where you need to prove what an agent actually did." to a more cautious formulation such as "Vaara is the tamper-evident runtime evidence layer for AI systems and supports evidence for EU AI Act compliance," so that the README's claim about EU AI Act applicability is framed as support rather than a guarantee; edit the phrasing directly in the README sentence to replace "covers EU AI Act compliance" with "supports evidence for EU AI Act compliance" (or similar wording) and keep the rest of the sentence intact.
🤖 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 `@docs/execution-receipts.md`:
- Around line 99-101: The fenced code block containing the command "python
tests/vectors/execution_receipt_v0/_check_independent.py" is untyped and
triggers markdownlint MD040; update that fenced block in
docs/execution-receipts.md by adding a language tag (e.g., bash) after the
opening backticks so the block reads as a bash fenced code block, ensuring the
linter no longer flags it.
In `@scripts/generate_receipt_vectors.py`:
- Around line 67-91: _emit_keys currently always generates new ES256/RS256 keys
and overwrites committed PEM fixtures; change it to reuse existing files under
OUT/"keys" when present and only generate and write new keys (and
hs256_secret.bin) if the files are missing. Specifically, have _emit_keys check
for the existence of "hs256_secret.bin", "es256_private.pem"/"es256_public.pem",
and "rs256_private.pem"/"rs256_public.pem" in the keys directory; if present,
read and deserialize the private PEMs with serialization.load_pem_private_key to
obtain the ec and rsa private key objects, otherwise generate the keys and write
the PEMs as before. Ensure directory creation remains (keys.mkdir) and return
the same dict shape {"ES256": es, "RS256": rs} with the loaded or newly
generated key objects.
In `@src/vaara/adversarial_classifier.py`:
- Around line 263-265: The current code JSON-dumps the entire entry and applies
_METADATA_SSRF_PATTERNS against that blob, which incorrectly elevates
probability when non-destination fields (like context/original_task) mention
metadata IPs; change the check to only scan destination-bearing values: iterate
entry's keys and for each value that is a string (or convert non-string
scalars/nested values individually to strings), run the existing
_METADATA_SSRF_PATTERNS against that single value, and only if a pattern matches
one of those values return max(prob, _METADATA_SSRF_FLOOR); keep using the same
names (entry, prob, _METADATA_SSRF_PATTERNS, _METADATA_SSRF_FLOOR) so the logic
targets host/URL-bearing fields instead of the whole blob.
- Around line 61-72: The current _METADATA_SSRF_PATTERNS use bare substring
regexes which over- and under-match; update each pattern to require
non-host/digit boundaries so they match the intended full host/IP/token only and
enable case-insensitive matching for hostnames: e.g. replace
re.compile(r"169\.254\.169\.254") and re.compile(r"169\.254\.170\.2") with
boundary-protected variants like
re.compile(r"(?<![A-Za-z0-9_.-])169\.254\.169\.254(?![A-Za-z0-9_.-])"),
re.compile(r"(?<![A-Za-z0-9_.-])169\.254\.170\.2(?![A-Za-z0-9_.-])"); make
metadata.google.internal case-insensitive and boundary-protected using
re.compile(r"(?<![A-Za-z0-9_.-])metadata\.google\.internal(?![A-Za-z0-9_.-])",
re.I); apply analogous boundary anchors to the IPv6 pattern (fd00:ec2::254) and
to the dotless decimal/hex patterns (2852039166 and 0xa9fea9fe) so they can’t be
matched as substrings of longer tokens while preserving the existing re.I where
needed, leaving the list name _METADATA_SSRF_PATTERNS untouched.
In `@src/vaara/attestation/_receipt_types.py`:
- Around line 213-235: The top-level alg and the nested receiptAsserted.alg must
be compared and rejected if they differ: in receipt_from_dict, after validating
d["alg"] and creating receipt_asserted via
receipt_asserted_from_dict(d["receiptAsserted"]), check that
receipt_asserted.alg (or its equivalent attribute) equals d["alg"] and raise
AttestationError with a clear message if not; alternatively implement the same
check in ExecutionReceipt.__post_init__ to enforce consistency on construction.
- Around line 93-106: OutcomeDerived allows an invalid combination of status and
result_commitment; add a __post_init__ on the OutcomeDerived dataclass to
validate the invariant: if status == ReceiptStatus.REFUSED then
result_commitment must be None, and if status is EXECUTED or ERRORED then
result_commitment must be present; if the check fails raise a ValueError with a
clear message. Implement the check inside OutcomeDerived.__post_init__ (no other
changes to callers), referencing the status and result_commitment attributes to
enforce the rule.
In `@tests/vectors/execution_receipt_v0/README.md`:
- Around line 9-22: Update the fenced code blocks in
tests/vectors/execution_receipt_v0/README.md to include language identifiers:
change the layout/tree fence to ```text (the block that lists keys/,
normative/<case>/, etc.) and change the command fence that runs
_check_independent.py to ```bash; apply the same text/bash annotations for the
other matching fence at lines referenced (37-39) so markdownlint stops flagging
them.
---
Nitpick comments:
In `@README.md`:
- Line 17: Update the sentence that currently reads "Vaara is the tamper-evident
runtime evidence layer for AI systems. It covers EU AI Act compliance, and any
other case where you need to prove what an agent actually did." to a more
cautious formulation such as "Vaara is the tamper-evident runtime evidence layer
for AI systems and supports evidence for EU AI Act compliance," so that the
README's claim about EU AI Act applicability is framed as support rather than a
guarantee; edit the phrasing directly in the README sentence to replace "covers
EU AI Act compliance" with "supports evidence for EU AI Act compliance" (or
similar wording) and keep the rest of the sentence intact.
🪄 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: 6391939a-54fe-4942-a234-8c0957a90343
⛔ Files ignored due to path filters (5)
tests/vectors/execution_receipt_v0/keys/es256_private.pemis excluded by!**/*.pemtests/vectors/execution_receipt_v0/keys/es256_public.pemis excluded by!**/*.pemtests/vectors/execution_receipt_v0/keys/hs256_secret.binis excluded by!**/*.bintests/vectors/execution_receipt_v0/keys/rs256_private.pemis excluded by!**/*.pemtests/vectors/execution_receipt_v0/keys/rs256_public.pemis excluded by!**/*.pem
📒 Files selected for processing (41)
.gitignoreCHANGELOG.mdREADME.mdclients/ts/package.jsondocs/execution-receipts.mdpyproject.tomlscripts/generate_receipt_vectors.pyserver-vaara-server.jsonserver.jsonsrc/vaara/__init__.pysrc/vaara/adversarial_classifier.pysrc/vaara/attestation/__init__.pysrc/vaara/attestation/_receipt_emit.pysrc/vaara/attestation/_receipt_types.pysrc/vaara/attestation/_receipt_verifier.pysrc/vaara/attestation/receipt.pytests/test_adversarial_classifier_integration.pytests/test_execution_receipt.pytests/test_metadata_ssrf_floor.pytests/test_receipt_vectors.pytests/vectors/execution_receipt_v0/README.mdtests/vectors/execution_receipt_v0/_check_independent.pytests/vectors/execution_receipt_v0/normative/es256_executed_projection/attestation.jsontests/vectors/execution_receipt_v0/normative/es256_executed_projection/expected.jsontests/vectors/execution_receipt_v0/normative/es256_executed_projection/receipt.jsontests/vectors/execution_receipt_v0/normative/es256_executed_projection/runtime_result.jsontests/vectors/execution_receipt_v0/normative/hs256_executed_digest/attestation.jsontests/vectors/execution_receipt_v0/normative/hs256_executed_digest/expected.jsontests/vectors/execution_receipt_v0/normative/hs256_executed_digest/receipt.jsontests/vectors/execution_receipt_v0/normative/hs256_executed_digest/runtime_result.jsontests/vectors/execution_receipt_v0/normative/neg_broken_backlink/attestation.jsontests/vectors/execution_receipt_v0/normative/neg_broken_backlink/expected.jsontests/vectors/execution_receipt_v0/normative/neg_broken_backlink/receipt.jsontests/vectors/execution_receipt_v0/normative/neg_broken_backlink/runtime_result.jsontests/vectors/execution_receipt_v0/normative/neg_result_mismatch/attestation.jsontests/vectors/execution_receipt_v0/normative/neg_result_mismatch/expected.jsontests/vectors/execution_receipt_v0/normative/neg_result_mismatch/receipt.jsontests/vectors/execution_receipt_v0/normative/neg_result_mismatch/runtime_result.jsontests/vectors/execution_receipt_v0/normative/rs256_refused_no_commitment/attestation.jsontests/vectors/execution_receipt_v0/normative/rs256_refused_no_commitment/expected.jsontests/vectors/execution_receipt_v0/normative/rs256_refused_no_commitment/receipt.json
| ``` | ||
| python tests/vectors/execution_receipt_v0/_check_independent.py | ||
| ``` |
There was a problem hiding this comment.
Add a language tag to the fenced command block.
This block triggers markdownlint MD040 because the fence is untyped.
Suggested patch
-```
+```bash
python tests/vectors/execution_receipt_v0/_check_independent.py</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 99-99: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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/execution-receipts.md` around lines 99 - 101, The fenced code block
containing the command "python
tests/vectors/execution_receipt_v0/_check_independent.py" is untyped and
triggers markdownlint MD040; update that fenced block in
docs/execution-receipts.md by adding a language tag (e.g., bash) after the
opening backticks so the block reads as a bash fenced code block, ensuring the
linter no longer flags it.
| def _emit_keys() -> dict: | ||
| es = ec.generate_private_key(ec.SECP256R1()) | ||
| rs = rsa.generate_private_key(public_exponent=65537, key_size=2048) | ||
| keys = OUT / "keys" | ||
| keys.mkdir(parents=True, exist_ok=True) | ||
| (keys / "hs256_secret.bin").write_bytes(HS_SECRET) | ||
| (keys / "es256_private.pem").write_bytes(es.private_bytes( | ||
| serialization.Encoding.PEM, | ||
| serialization.PrivateFormat.PKCS8, | ||
| serialization.NoEncryption(), | ||
| )) | ||
| (keys / "es256_public.pem").write_bytes(es.public_key().public_bytes( | ||
| serialization.Encoding.PEM, | ||
| serialization.PublicFormat.SubjectPublicKeyInfo, | ||
| )) | ||
| (keys / "rs256_private.pem").write_bytes(rs.private_bytes( | ||
| serialization.Encoding.PEM, | ||
| serialization.PrivateFormat.PKCS8, | ||
| serialization.NoEncryption(), | ||
| )) | ||
| (keys / "rs256_public.pem").write_bytes(rs.public_key().public_bytes( | ||
| serialization.Encoding.PEM, | ||
| serialization.PublicFormat.SubjectPublicKeyInfo, | ||
| )) | ||
| return {"ES256": es, "RS256": rs} |
There was a problem hiding this comment.
Keep the committed vector keys stable across regenerations.
_emit_keys() always creates fresh ES256/RS256 keypairs and overwrites the committed PEMs, so rerunning this script rewrites every asymmetric fixture even when the wire format is unchanged. That makes the conformance vectors non-reproducible and causes noisy fixture churn in a place that is supposed to stay pinned. Reuse the checked-in keys when they already exist, and only generate them on first bootstrap.
Suggested direction
def _emit_keys() -> dict:
+ keys = OUT / "keys"
+ keys.mkdir(parents=True, exist_ok=True)
+
+ es_priv = keys / "es256_private.pem"
+ rs_priv = keys / "rs256_private.pem"
+ hs_key = keys / "hs256_secret.bin"
+ if es_priv.exists() and rs_priv.exists() and hs_key.exists():
+ return {
+ "ES256": serialization.load_pem_private_key(
+ es_priv.read_bytes(), password=None
+ ),
+ "RS256": serialization.load_pem_private_key(
+ rs_priv.read_bytes(), password=None
+ ),
+ }
+
es = ec.generate_private_key(ec.SECP256R1())
rs = rsa.generate_private_key(public_exponent=65537, key_size=2048)
- keys = OUT / "keys"
- keys.mkdir(parents=True, exist_ok=True)
(keys / "hs256_secret.bin").write_bytes(HS_SECRET)🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 72-72: Clear-text storage of sensitive information
This expression stores sensitive data (secret) as clear text.
🤖 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 `@scripts/generate_receipt_vectors.py` around lines 67 - 91, _emit_keys
currently always generates new ES256/RS256 keys and overwrites committed PEM
fixtures; change it to reuse existing files under OUT/"keys" when present and
only generate and write new keys (and hs256_secret.bin) if the files are
missing. Specifically, have _emit_keys check for the existence of
"hs256_secret.bin", "es256_private.pem"/"es256_public.pem", and
"rs256_private.pem"/"rs256_public.pem" in the keys directory; if present, read
and deserialize the private PEMs with serialization.load_pem_private_key to
obtain the ec and rsa private key objects, otherwise generate the keys and write
the PEMs as before. Ensure directory creation remains (keys.mkdir) and return
the same dict shape {"ES256": es, "RS256": rs} with the loaded or newly
generated key objects.
| _METADATA_SSRF_PATTERNS = ( | ||
| re.compile(r"169\.254\.169\.254"), | ||
| re.compile(r"metadata\.google\.internal"), | ||
| re.compile(r"169\.254\.170\.2"), | ||
| # AWS IMDS over IPv6 link-local. | ||
| re.compile(r"fd00:ec2::254", re.I), | ||
| # 169.254.169.254 as a dotless 32-bit decimal integer, bounded so it is not | ||
| # a slice of a longer digit run. | ||
| re.compile(r"(?<!\d)2852039166(?!\d)"), | ||
| # 169.254.169.254 as a dotless hex integer; the 0x prefix anchors it. | ||
| re.compile(r"0xa9fea9fe(?![0-9a-f])", re.I), | ||
| ) |
There was a problem hiding this comment.
Tighten the metadata regexes before relying on them for a hard floor.
These are still substring matches: 169.254.170.20 will hit Line 64, metadata.google.internal.evil.com will hit Line 63, and mixed-case GCP hosts currently miss entirely. That makes the floor both over- and under-inclusive for the exact destinations it is supposed to pin down.
Suggested fix
_METADATA_SSRF_PATTERNS = (
- re.compile(r"169\.254\.169\.254"),
- re.compile(r"metadata\.google\.internal"),
- re.compile(r"169\.254\.170\.2"),
+ re.compile(r"(?<!\d)169\.254\.169\.254(?!\d)"),
+ re.compile(r"(?<![A-Za-z0-9-])metadata\.google\.internal(?![A-Za-z0-9.-])", re.I),
+ re.compile(r"(?<!\d)169\.254\.170\.2(?!\d)"),🤖 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/adversarial_classifier.py` around lines 61 - 72, The current
_METADATA_SSRF_PATTERNS use bare substring regexes which over- and under-match;
update each pattern to require non-host/digit boundaries so they match the
intended full host/IP/token only and enable case-insensitive matching for
hostnames: e.g. replace re.compile(r"169\.254\.169\.254") and
re.compile(r"169\.254\.170\.2") with boundary-protected variants like
re.compile(r"(?<![A-Za-z0-9_.-])169\.254\.169\.254(?![A-Za-z0-9_.-])"),
re.compile(r"(?<![A-Za-z0-9_.-])169\.254\.170\.2(?![A-Za-z0-9_.-])"); make
metadata.google.internal case-insensitive and boundary-protected using
re.compile(r"(?<![A-Za-z0-9_.-])metadata\.google\.internal(?![A-Za-z0-9_.-])",
re.I); apply analogous boundary anchors to the IPv6 pattern (fd00:ec2::254) and
to the dotless decimal/hex patterns (2852039166 and 0xa9fea9fe) so they can’t be
matched as substrings of longer tokens while preserving the existing re.I where
needed, leaving the list name _METADATA_SSRF_PATTERNS untouched.
| blob = json.dumps(entry, default=str) | ||
| if any(p.search(blob) for p in _METADATA_SSRF_PATTERNS): | ||
| return max(prob, _METADATA_SSRF_FLOOR) |
There was a problem hiding this comment.
Only apply the floor against destination-bearing fields, not the whole entry blob.
Line 263 includes context, so a benign call like write_file with original_task="document 169.254.169.254" will be forced to 0.95 even though nothing is being fetched from metadata. This should key off URL/host-bearing parameters, otherwise the classifier starts blocking mentions instead of destinations.
Suggested fix
- blob = json.dumps(entry, default=str)
+ blob = json.dumps(parameters or {}, default=str)
if any(p.search(blob) for p in _METADATA_SSRF_PATTERNS):
return max(prob, _METADATA_SSRF_FLOOR)🤖 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/adversarial_classifier.py` around lines 263 - 265, The current code
JSON-dumps the entire entry and applies _METADATA_SSRF_PATTERNS against that
blob, which incorrectly elevates probability when non-destination fields (like
context/original_task) mention metadata IPs; change the check to only scan
destination-bearing values: iterate entry's keys and for each value that is a
string (or convert non-string scalars/nested values individually to strings),
run the existing _METADATA_SSRF_PATTERNS against that single value, and only if
a pattern matches one of those values return max(prob, _METADATA_SSRF_FLOOR);
keep using the same names (entry, prob, _METADATA_SSRF_PATTERNS,
_METADATA_SSRF_FLOOR) so the logic targets host/URL-bearing fields instead of
the whole blob.
| @dataclass(frozen=True) | ||
| class OutcomeDerived: | ||
| """Facts about what happened to the attested request. | ||
|
|
||
| ``status`` is one of ``executed`` / ``refused`` / ``errored``. | ||
| ``result_commitment`` is a commitment over the result payload and | ||
| is optional: a refused call has no result, so the commitment is | ||
| absent. An executed or errored call commits to the result or the | ||
| error object respectively. | ||
| """ | ||
|
|
||
| status: ReceiptStatus | ||
| completed_at: str | ||
| result_commitment: Optional[ResultCommitment] = None |
There was a problem hiding this comment.
Enforce the refused/no-commitment invariant in the type itself.
OutcomeDerived currently allows status="refused" together with a result_commitment, even though this docstring defines that state as invalid. That means emit_receipt() can generate non-conformant receipts and parse_receipt() will also accept them. A small __post_init__ guard here would close both paths at once.
Suggested fix
`@dataclass`(frozen=True)
class OutcomeDerived:
@@
status: ReceiptStatus
completed_at: str
result_commitment: Optional[ResultCommitment] = None
+
+ def __post_init__(self) -> None:
+ if self.status == "refused" and self.result_commitment is not None:
+ raise AttestationError(
+ "outcomeDerived.resultCommitment must be absent when status='refused'"
+ )📝 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.
| @dataclass(frozen=True) | |
| class OutcomeDerived: | |
| """Facts about what happened to the attested request. | |
| ``status`` is one of ``executed`` / ``refused`` / ``errored``. | |
| ``result_commitment`` is a commitment over the result payload and | |
| is optional: a refused call has no result, so the commitment is | |
| absent. An executed or errored call commits to the result or the | |
| error object respectively. | |
| """ | |
| status: ReceiptStatus | |
| completed_at: str | |
| result_commitment: Optional[ResultCommitment] = None | |
| `@dataclass`(frozen=True) | |
| class OutcomeDerived: | |
| """Facts about what happened to the attested request. | |
| ``status`` is one of ``executed`` / ``refused`` / ``errored``. | |
| ``result_commitment`` is a commitment over the result payload and | |
| is optional: a refused call has no result, so the commitment is | |
| absent. An executed or errored call commits to the result or the | |
| error object respectively. | |
| """ | |
| status: ReceiptStatus | |
| completed_at: str | |
| result_commitment: Optional[ResultCommitment] = None | |
| def __post_init__(self) -> None: | |
| if self.status == "refused" and self.result_commitment is not None: | |
| raise AttestationError( | |
| "outcomeDerived.resultCommitment must be absent when status='refused'" | |
| ) |
🤖 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/_receipt_types.py` around lines 93 - 106,
OutcomeDerived allows an invalid combination of status and result_commitment;
add a __post_init__ on the OutcomeDerived dataclass to validate the invariant:
if status == ReceiptStatus.REFUSED then result_commitment must be None, and if
status is EXECUTED or ERRORED then result_commitment must be present; if the
check fails raise a ValueError with a clear message. Implement the check inside
OutcomeDerived.__post_init__ (no other changes to callers), referencing the
status and result_commitment attributes to enforce the rule.
| def receipt_from_dict(d: dict[str, Any]) -> ExecutionReceipt: | ||
| """Reconstruct an ExecutionReceipt from its wire JSON dict. | ||
|
|
||
| Inverse of ``ExecutionReceipt.to_dict()``. Field-presence | ||
| validation only; signature verification still requires the | ||
| caller's keying material. | ||
| """ | ||
| for required in ( | ||
| "version", "alg", "backLink", "outcomeDerived", | ||
| "receiptAsserted", "signature", | ||
| ): | ||
| if required not in d: | ||
| raise AttestationError(f"receipt missing required field {required!r}") | ||
| if d["alg"] not in VALID_ALGS: | ||
| raise AttestationError(f"unsupported alg {d['alg']!r}") | ||
| return ExecutionReceipt( | ||
| version=d["version"], | ||
| alg=d["alg"], | ||
| back_link=back_link_from_dict(d["backLink"]), | ||
| receipt_asserted=receipt_asserted_from_dict(d["receiptAsserted"]), | ||
| outcome_derived=outcome_from_dict(d["outcomeDerived"]), | ||
| signature=d["signature"], | ||
| ) |
There was a problem hiding this comment.
Reject receipts whose two alg fields disagree.
parse_receipt() validates the top-level alg and receiptAsserted.alg separately, but never checks they are equal. Since signature verification later dispatches on receipt.alg, an envelope can verify while still advertising a different algorithm inside receiptAsserted. Please fail fast here (or in ExecutionReceipt.__post_init__) when the fields diverge.
Suggested fix
def receipt_from_dict(d: dict[str, Any]) -> ExecutionReceipt:
@@
if d["alg"] not in VALID_ALGS:
raise AttestationError(f"unsupported alg {d['alg']!r}")
+ if d["receiptAsserted"]["alg"] != d["alg"]:
+ raise AttestationError("receiptAsserted.alg must match receipt.alg")
return ExecutionReceipt(
version=d["version"],
alg=d["alg"],🤖 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/_receipt_types.py` around lines 213 - 235, The
top-level alg and the nested receiptAsserted.alg must be compared and rejected
if they differ: in receipt_from_dict, after validating d["alg"] and creating
receipt_asserted via receipt_asserted_from_dict(d["receiptAsserted"]), check
that receipt_asserted.alg (or its equivalent attribute) equals d["alg"] and
raise AttestationError with a clear message if not; alternatively implement the
same check in ExecutionReceipt.__post_init__ to enforce consistency on
construction.
| ``` | ||
| keys/ pinned signing material | ||
| hs256_secret.bin 32 raw bytes | ||
| es256_private.pem PKCS8, raw r||s signatures (not DER) | ||
| es256_public.pem SubjectPublicKeyInfo | ||
| rs256_private.pem PKCS8 | ||
| rs256_public.pem SubjectPublicKeyInfo | ||
| normative/<case>/ | ||
| attestation.json the SEP-2787 attestation the receipt answers | ||
| receipt.json the signed execution receipt | ||
| runtime_result.json the result object (commitment cases only) | ||
| expected.json {signature_ok, back_link_ok, result_commitment_ok} | ||
| _check_independent.py stdlib + cryptography + rfc8785, no Vaara import | ||
| ``` |
There was a problem hiding this comment.
Add languages to the fenced code blocks.
markdownlint is already flagging both fences here, so this doc change will keep the style check noisy until they are annotated. text for the layout tree and bash for the command block would fix it.
Suggested fix
-```
+```text
keys/ pinned signing material
hs256_secret.bin 32 raw bytes
es256_private.pem PKCS8, raw r||s signatures (not DER)
es256_public.pem SubjectPublicKeyInfo
rs256_private.pem PKCS8
rs256_public.pem SubjectPublicKeyInfo
normative/<case>/
attestation.json the SEP-2787 attestation the receipt answers
receipt.json the signed execution receipt
runtime_result.json the result object (commitment cases only)
expected.json {signature_ok, back_link_ok, result_commitment_ok}
_check_independent.py stdlib + cryptography + rfc8785, no Vaara import@@
- +bash
python tests/vectors/execution_receipt_v0/_check_independent.py
Also applies to: 37-39
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 9-9: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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/vectors/execution_receipt_v0/README.md` around lines 9 - 22, Update the
fenced code blocks in tests/vectors/execution_receipt_v0/README.md to include
language identifiers: change the layout/tree fence to ```text (the block that
lists keys/, normative/<case>/, etc.) and change the command fence that runs
_check_independent.py to ```bash; apply the same text/bash annotations for the
other matching fence at lines referenced (37-39) so markdownlint stops flagging
them.
Execution receipts are the post-execution sibling of the SEP-2787 request attestation. SEP-2787 binds a tools/call request before it runs; the receipt covers the deferred half, what happened to it. The two together give end-to-end accountability for one action. The envelope is three blocks (backLink, receiptAsserted, outcomeDerived) plus a signature, reusing the SEP-2787 RFC 8785 canonicalization and the HS256/ES256/RS256 signing stack unchanged. A verifier that already checks SEP-2787 signatures needs no new crypto for receipts. backLink pins the attestation by nonce and by a digest over its full wire bytes. outcomeDerived carries the status, completion time, and an optional result commitment reusing the argument-commitment shapes. A receipt is a durable record rather than a time-bounded capability, so there is no TTL. Ships with v0 conformance vectors (pinned keys, five cases across all three algorithms plus negative back-link and result-mismatch cases) and a stdlib-only independent walker that verifies them without importing Vaara. Library surface only this release; pairing both envelopes into the MCP proxy emission path is the next increment. Also folds a deterministic floor for cloud instance-metadata endpoints into the classifier. The v9 model underweighted a bare http_post to AWS IMDS, the GCP metadata server, and ECS task-role, scoring them below the calibrated threshold. The floor lifts them above it regardless of model output, defense-in-depth on top of the learned score. The match list covers the parser-confusion encodings that slip a literal check too: the IPv6 link-local address AWS serves IMDS on, and the dotless decimal and hex forms of the IMDS IPv4 address, each bounded against a longer digit or hex run. Not exhaustive on purpose: IMDSv2 token flows, DNS rebinding, and arbitrary octal or mixed encodings still fall back to the model. 946 passed, 12 skipped, ruff clean. All version surfaces at 0.42.0. Co-authored-by: vaaraio <[email protected]>
v0.42.0: execution receipts paired with SEP-2787
Execution receipts are the post-execution sibling of the SEP-2787 request attestation. SEP-2787 binds a
tools/callrequest before it runs. The receipt covers the deferred half, what happened to it. Together they give end-to-end accountability for one action.Added
vaara.attestation.receipt: a signed execution-receipt envelope binding the outcome of one attestedtools/calland linking back to the attestation it answers.backLink,receiptAsserted,outcomeDerived) plus a signature, reusing the SEP-2787 canonicalization (RFC 8785 JCS) and signing stack (HS256 / ES256 / RS256) unchanged. A verifier that already checks SEP-2787 signatures needs no new crypto for receipts.backLinkpins the attestation by nonce and by a digest over its full wire bytes.outcomeDerivedcarries the status (executed/refused/errored), completion time, and an optional result commitment that reuses the SEP-2787 argument-commitment shapes (payload stays local).verify_receipt_signature,verify_back_link, and the existingverify_args_commitmentfor the result binding. A receipt is a durable record rather than a time-bounded capability, so there is no TTL.tests/vectors/execution_receipt_v0/: pinned keys, five cases (positive across all three algorithms, refused-without-commitment, result-mismatch, broken back-link), and a stdlib-only independent walker that verifies them without importing Vaara.docs/execution-receipts.mddocuments the format, emission, verification, and the relationship to OVERT 1.0 Part 3.Fixed
AdversarialClassifier.scorenow applies a deterministic floor for cloud instance-metadata endpoints (AWS IMDS, GCP metadata server, ECS task-role). The v9 model underweighted a barehttp_postto these known credential-theft destinations, scoring them below the calibrated threshold. The floor lifts them above it regardless of model output, defense-in-depth on top of the learned score. The match list also covers the parser-confusion encodings that slip a literal-string check: the IPv6 link-local address AWS serves IMDS on, and the dotless decimal and hex forms of the IMDS IPv4 address, each bounded so it cannot fire on a longer digit or hex run. Not exhaustive on purpose: IMDSv2 token flows, DNS rebinding, and arbitrary octal or mixed encodings still fall back to the model. The supportable claim is that Vaara flags the well-known cloud instance-metadata endpoints.Notes
Library surface only this release. Pairing both the SEP-2787 request attestation and the receipt into the MCP proxy emission path is the next increment. A receipt without its attestation persisted is not a coherent artifact, so the attestation side lands properly rather than bolted on.
Validation
946 passed, 12 skipped, ruff clean. All version surfaces at 0.42.0 (pyproject,
__init__.py, clients/ts/package.json, both MCP Registry manifests).Summary by CodeRabbit
Release Notes v0.42.0
New Features
Improvements
Documentation