Repository navigation
v2.9.0
[2.9.0] - 2026-10-10
Added
-
Conformance vectors for the cage block of a decision receipt,
tests/vectors/cage_v0/, with a standalone checker that gives each file a signature, evidence and cage verdict. The rules of the block are written out in SPEC.md section 5.10:driver: nonecarriesconfirmed: falseand nothing else, any other driver carries abasis,confirmed: trueneeds a basis that names the fact that was read, andconfigDigestissha256:and 64 lowercase hex. -
SPEC.md Sections 1 to 4 describe the whole envelope the library reads and writes: both receipt kinds, every member of each block with its type, the closed-schema rule, the issuer block (
aud,taskId,completeness,sigSuite,cryptoPosture),pqSignature,existenceProof, theerroredstatus,decisionDigest, both result-commitment forms, the decision basis (clientTurnId,rationale,binding,decisionProof), and the blinded and Merkle-log anchor methods. Theledgeranchor method is removed from the registry: nothing produced or verified it. The CDDL schema of the envelope isietf/vaara-receipt.cddl, checked against every receipt in the vectors.
Fixed
- The OS guard no longer stops on the first decision it records. The guard holds every open and exec on the operator's mount until one of its reader threads answers, and a reader needs the GIL to answer. The signed receipt written beside each decision imported the signing libraries on first use, in a worker thread, after the marks were on. CPython holds the GIL across
dlopen, so the open of the shared object raised an event the readers could not answer, and from then on every process on the mount waited at its next open or exec, the CI runner included: the job wrote no log and could not be cancelled.start()now imports the receipt code, loads the signing key and signs once, before any mark goes on. A unit test runs one decision of each kind against the real trail with the signed receipt and fails when a decision loads new compiled code. The CI job installed Vaara without its dependencies and so had norfc8785; it never took the receipt path and passed. A plainpip install vaarahas both libraries, so a guard from it stopped on its first decision. - The anchor-log lock on Windows waits as long as it takes, as
flockdoes on POSIX. The C runtime's blocking lock gives up after ten one-second tries and raises EDEADLK, so a second anchor writer under load failed after ten seconds. The lock now polls the non-blocking form until it is granted and raises on any error other than contention. - A heredoc body that the same Bash command writes to a file and then runs is read by the shell rules again. The 2.8.0 rule that drops a body only
catorteeread judged the heredoc's own statement, socat > x.sh <<EOF ... EOF; bash x.shpassed every shell rule on the hook, the MCP proxy and the model-proxy tool gate. A written body now stays dropped only when nothing after the heredoc's statement names the file it wrote or a command that runs code (bash,sh,dash,zsh,ksh,fish,source,.,exec,eval,chmod,python*,node). Notes written withcat > notes.md <<EOFfollowed bygit statusare still text; the same notes followed bygit add notes.mdare read, since the rule does not tell a path that is staged from one that is run. A Python heredoc that calls the shell from inside its body remains the documented trade-off, pinned by a test. - Authority decay replays the whole session. The pipeline handed the replay the agent's last 100 trail records, four to five per action, so spend older than about twenty actions was forgotten and a session could outrun its own decay by making calls; a reviewer recomputing from the chain got a different budget than the pipeline used. The replay now reads every record of the session, with no limit, through the trail's new
get_session_records. The module notes the one caveat that remains: a long-running proxy does not see records another process wrote to the same session until it reloads. - The SOC 2 rows of the default compliance engine are no longer critical. 2.8.0 added them with CC6.1, CC6.3, CC7.2 and CC7.3 critical, so a trail that reported no critical gaps on 2.7.1 reported SOC 2 critical gaps on 2.8.0 with nothing changed on the operator's side, most visibly a trail under twenty scored actions. The six criteria are still assessed in every default report; a deployer whose own SOC 2 report covers its agents marks the rows critical, as
docs/COMPLIANCE.mdnow shows. - An egress upstream that does not answer is no longer recorded as a policy denial. The proxy rewrote the event as refused, and
vaara runfiled it with risk 1.0 and anegress_not_allowedviolation, with a signed receipt saying block, for what was a 502. An allowed connection is now a decision at the moment it is allowed, before the connect, so a long stream is on the trail while it runs and stays there ifvaara rundies; it is closed by an outcome with the bytes moved, or by an outcome saying the upstream did not answer. The proxy's events carrykind(refused,opened,closed,failed) and aconnectionid shared by the three allowed kinds. vaara cage statusfor a gVisor or Kata launch reports the sameconfig_digestthe receipts from inside carry. The driver gave the container the digest of its request and then reported the digest of the engine's own view of the container under the same name, so comparing a receipt against the status of its own launch always mismatched. The state now reads the given digest back from the container's environment, and the engine's view stays beside it asactive_digest.- The egress proxy refuses an allowed name that resolves to a private range (10/8, 172.16/12, 192.168/16, 100.64/10, fc00::/7), as it already did for loopback, link-local, multicast and the unspecified address. An entry that names the address literally still passes, so an internal service is reached by writing its address into the allow list.
- The OS-layer selection says which egress entry it dropped. An entry the proxy could not parse (a URL, a host with a space) was skipped without a word, so the allow list was narrower than written and the operator found out from a 403. The entry and the reason are now logged as a warning when the selection is read.
- Cage configuration digests for the engine, E2B and the other JSON-request drivers use the same canonical form as the receipts (sorted keys, no spaces, UTF-8, floats refused). They were sorted JSON with floats allowed; for the requests the drivers make, the bytes are the same, so no existing digest changes.
tests/vectors/fallback_projection_v0/hashed{arguments, authBinding, toolName}, a shape no signed record used. It now holds the SEP-2828 projection the library and the signed receipts indecision_pairing_v0use,tools_call_params_plus_meta_authorization_binding_v1, starting from raw request envelopes with different_metasidecars, and adds the cases that must fail closed: no binding block, a binding without a nonce, a binding that is not an object, missing arguments, and an unknown projection version. The checker now canonicalizes with RFC 8785.- A receipt whose issuer block names a different
algthan its envelope no longer verifies. Both are inside the signed bytes, andverify_decision_signatureandverify_receipt_signaturenow refuse the pair before trying a key, as the record conformance check already did. - A decision record is a closed schema, like the execution receipt. A member the format does not define, at the top level or inside
decisionDerived, is refused when the record is parsed instead of being dropped and then failing the signature check with no reason given. - Three more envelope rules the library now holds when it parses a receipt:
pqSignatureis a closed block, so a member it does not define is refused like one anywhere else;versionmust be1on both receipt kinds, so a later version is never read under v1 rules; and a decision record carryingissuerAsserted.sigSuiteis refused, since there is nopqSignatureon a decision for the suite to commit to. - SPEC.md and the draft said the authorization profile's
grantFingerprintis a digest over the signed grant. It is a digest over the grant with itssignaturemember removed, which is what the library and theauthorization_v0checker compute. The disposition members (approver,humanDisposed) are described where they exist, in thevaara.trail-decision/v0evidence record, not invaara.authorization/v0. - Decision binding digests always use RFC 8785. Without
rfc8785installed they fell back to sorted JSON with a warning, which gives digests another implementation cannot reproduce; they now raise instead.rfc8785is a base dependency, so a working install never took that path. - A decision receipt never states more about its cage than the rules allow. A block passed to
record_decisionby custom code with a malformedconfigDigest, or withconfirmed: trueondriver: noneor on adeclaredbasis, was copied into the signed receipt as given. The receipt now leaves the digest out and writesconfirmed: false; the trail record keeps what it was given. - Deny rules that name a file path hold on Windows. Their patterns are written with
/, so a path a Windows harness sends, such asC:\Users\me\.ssh\id_ed25519, passedsecret_material_readand every other path rule. Path arguments are now matched with their separators turned, for named tools, harness aliases and MCP arguments alike. - Text that Vaara reads and writes is UTF-8 with
\nline endings on every platform. On Windows the code page decoded agent configs that carry a non-ASCII path or user name wrong, and text mode wrote\r\n, so the trail export, receipt files and the decision vocabulary vectors were different bytes there than on Linux and macOS. - The cage layer loads on Windows. The OS guard imported
fcntlandpwdat module load, so every module that reads its socket path failed to import,vaara cage driversincluded. vaara scanattributes an MCP config to its agent on Windows and shows its path with/. It matched the agent directories with/against a path written with\.- Trail repair leaves the trail in place when another process holds it open on Windows. It raised
PermissionErrorhalf way through moving the files; it now puts back what it moved and reports the repair as failed. - A directory given as a policy file is reported as a directory on Windows, where opening one raises
PermissionError. - The pipeline times each decision with
perf_counter. The monotonic clock's resolution on Windows reported most decisions as taking 0.0 ms.