Skip to content

v2.9.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 22:52
· 89 commits to main since this release
6243e36

[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: none carries confirmed: false and nothing else, any other driver carries a basis, confirmed: true needs a basis that names the fact that was read, and configDigest is sha256: 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, the errored status, decisionDigest, both result-commitment forms, the decision basis (clientTurnId, rationale, binding, decisionProof), and the blinded and Merkle-log anchor methods. The ledger anchor method is removed from the registry: nothing produced or verified it. The CDDL schema of the envelope is ietf/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 no rfc8785; it never took the receipt path and passed. A plain pip install vaara has 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 flock does 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 cat or tee read judged the heredoc's own statement, so cat > x.sh <<EOF ... EOF; bash x.sh passed 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 with cat > notes.md <<EOF followed by git status are still text; the same notes followed by git add notes.md are 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.md now 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 run filed it with risk 1.0 and an egress_not_allowed violation, 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 if vaara run dies; it is closed by an outcome with the bytes moved, or by an outcome saying the upstream did not answer. The proxy's events carry kind (refused, opened, closed, failed) and a connection id shared by the three allowed kinds.
  • vaara cage status for a gVisor or Kata launch reports the same config_digest the 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 as active_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 in decision_pairing_v0 use, tools_call_params_plus_meta_authorization_binding_v1, starting from raw request envelopes with different _meta sidecars, 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 alg than its envelope no longer verifies. Both are inside the signed bytes, and verify_decision_signature and verify_receipt_signature now 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: pqSignature is a closed block, so a member it does not define is refused like one anywhere else; version must be 1 on both receipt kinds, so a later version is never read under v1 rules; and a decision record carrying issuerAsserted.sigSuite is refused, since there is no pqSignature on a decision for the suite to commit to.
  • SPEC.md and the draft said the authorization profile's grantFingerprint is a digest over the signed grant. It is a digest over the grant with its signature member removed, which is what the library and the authorization_v0 checker compute. The disposition members (approver, humanDisposed) are described where they exist, in the vaara.trail-decision/v0 evidence record, not in vaara.authorization/v0.
  • Decision binding digests always use RFC 8785. Without rfc8785 installed they fell back to sorted JSON with a warning, which gives digests another implementation cannot reproduce; they now raise instead. rfc8785 is 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_decision by custom code with a malformed configDigest, or with confirmed: true on driver: none or on a declared basis, was copied into the signed receipt as given. The receipt now leaves the digest out and writes confirmed: 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 as C:\Users\me\.ssh\id_ed25519, passed secret_material_read and 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 \n line 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 fcntl and pwd at module load, so every module that reads its socket path failed to import, vaara cage drivers included.
  • vaara scan attributes 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 PermissionError half 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.