Skip to content

v0.32.0

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Oct 20:10
· 1 commit to main since this release
582d158
  • A redacted trace verifies as a redacted trace. Removing an artifact's envelope from a sealed package was indistinguishable from never having had it: the Merkle tree still commits to the id, so the verifier reported an envelope that should be there and was not. That is the right answer for tampering and the wrong one for sharing a trace with parts withheld. A package may now carry redaction.json, a statement the producer signs listing what it withheld and the parent each withheld step named. The verifier honours it only when the signature verifies under a key the package is already judged by and the statement names this package's session and Merkle root, so an unsigned marker cannot excuse a stripped step and a redaction cannot be lifted from one trace onto another. A withheld artifact reports as withheld rather than missing, the chain still walks, and chain_linkage warns that the edges across withheld steps are the producer's assertion rather than a signed link, because the link lives inside the envelope being withheld. Any withheld artifact caps the verdict at the new verified-partial: everything disclosed checks out and the reader is holding part of a trace, which the word now says.
  • treeship package redact writes a copy of a trace with chosen steps withheld. Three things leave the copy for each withheld artifact: its signed envelope, its timeline entries, and its recorded process executions. The third matters most, because the envelope holds the argv and the digests while the receipt holds the command, so withholding the envelope alone would have hidden the proof and left the thing being proved in the clear. Entries are found by the artifact_id they now carry rather than by matching timestamps or names, since a redaction that silently misses an entry tells someone their data is gone when it is not. preview.html is re-rendered from the redacted receipt, because it embeds the receipt and a stale one would serve the withheld content straight back. The original close record is kept rather than re-signed: it still proves what was sealed at close, and the signed redaction names both receipt digests, so the derived view is provably the producer's own. The copy reads as verified-partial.
  • The content layer: the bytes behind a digest. A step receipt commits to its output by digest, not by value, which is what lets a trace be handed to someone who should not see the data -- and also what left a reader holding a commitment to something nobody could open. Packages may now carry the bytes in content/, one file per blob, each named by the SHA-256 of its own contents. A blob is bound to the trace because a signed statement in the package already names its digest, and that is the entire binding: nothing new is signed, and treeship package content add refuses bytes the trace does not name, so attaching content long after sealing and dropping it before sharing each leave every signature, the Merkle root and the chain exactly as they were. treeship package content ls lists what the trace commits to and how much of it is openable here; get recomputes the hash before it hands anything over and writes to stdout by default, so the bytes can be piped into shasum -a 256 and checked by someone who trusts neither the package nor this binary; drop removes bytes and keeps the commitment to them.
  • package verify gains a content row, which fails on two things. A blob whose bytes do not hash to the name it is filed under -- that is a substitution no signature would catch, because nothing signed covers the file -- and a blob no signed statement names, which is an attachment the trace does not account for rather than part of it. A reader who finds bytes inside a package reads them as part of it, so that one fails rather than warns. A trace that commits to more than it carries is not a failure: content is optional and dropping it is supported, so the row reports the ratio and leaves the judgement to the reader. Packages with no content/ directory get no row at all.
  • treeship package redact now takes a withheld step's content with it. The digest that justified a blob lives inside the envelope being withheld, so leaving the bytes behind would disclose exactly what was meant to be withheld and fail the copy's own content row. The surviving digests are recomputed from what is left in the copy rather than from the withheld envelopes, so a digest two steps both name survives the withholding of one of them.
  • A receipt's own account of a session now points at the evidence for it. Timeline entries and recorded process executions carried no link back to the artifact they were sealed as, so nothing in the narrative could be traced to the signed record behind it. That gap blocks sharing a trace with parts withheld: a producer withholding an artifact had no way to find the entries describing it, and the command it meant to withhold stayed in side_effects.processes in the clear. Matching by timestamp or name would have been a guess, and a redaction that silently misses an entry is worse than none. TimelineEntry and ProcessExecution now carry an optional artifact_id, wrap names the artifact it just sealed on its completion event, and the receipt composer propagates it. Absent on events that seal nothing, and on every receipt written before the field existed.
  • The session receipt carries what the judges said. A session's judgement.v1 receipts were sealed into the package and read by package verify, but nothing about them reached receipt.json, so every consumer holding the receipt without the envelopes, the report, the pull-request comment and anything reading the JSON, was blind to them. session close now fills a judgements section with the total, the distinct judges by the same name the verifier reports, and separate counts for refused, escalated and resolved. The counts are kept apart deliberately: a refusal, an open escalation and a closed one mean different things to a reviewer, and one number lets the worst of them hide in the total. A session nobody judged has no section at all rather than a band of zeroes, because silence and a clean bill are not the same claim. The section records what the caller's own record says and never re-runs a judge; the package verifier still derives the same facts independently from the envelopes, which are what is signed.
  • A pull request now carries its own verdict. verify-receipts verified every sealed package and then wrote the result to a step summary behind a click into the Actions tab, which is the same place nobody looks, so a reviewer read an agent's own description of its work and nothing else. The action now posts a verdict comment on the pull request and edits that comment in place on later runs: a monospace block saying whether the trace verified and over how many receipts, what the agent did in counts, and, last, what was not captured, with the offline verify command. Lines whose evidence is not in the package, judgements and signed approvals, are omitted rather than invented. Commenting needs pull-requests: write; a fork pull request gets a read-only token, where the comment is skipped with a warning and the verdict still stands in the log, the annotations and the summary. A comment never decides whether the check passes.
  • The person who owns the receipt is now told it exists. session close printed the package path only inside a package verify hint, never named the human-readable report, and never mentioned treeship dashboard; the Claude Code plugin's SessionEnd hook announced the sealed session through additionalContext, which is injected into the model's prompt and shown to nobody. The net effect was that a session sealed correctly and silently, and the person whose work it recorded had no way to learn it had happened. session close now prints report: with the path to preview.html and hints treeship dashboard alongside the verify and publish paths, and the JSON output gains a report field so callers do not have to reconstruct the path. The SessionEnd hook reads that field from the close it just performed and emits the path in systemMessage, the documented user-facing field, while additionalContext now instructs Claude to say it out loud as well, because the hooks reference does not state whether systemMessage renders for SessionEnd. One of the two always reaches the person.
  • The session report is legible at the size people actually read it. The receipt preview was laid out as a printed document: a 680px column at 14.5px with a long tail of sub-11px labels, which on screen read as timid rather than quiet. The document framing stays, the scale does not: an 880px column, a verdict set at up to 3.9rem, metrics at up to 2.9rem, serif section headings in place of micro-caps eyebrows, and nothing below 0.68rem. Commands and the raw receipt now render on the dark evidence surface the design system reserves for technical output, with the brand green on the shell line, so the page distinguishes narrative from evidence at a glance. Findings, checks and badges carry colour and a tinted ground instead of a hairline outline. Print rules were rewritten to match, and nothing in the rendering or verification logic changed: the client-side Merkle recomputation, the escaping every receipt-derived string passes through, and the proven-versus-not-proven panel are untouched.
  • RFC 8785 canonical JSON, beside the encoding Treeship signs. core::jcs adds the JSON Canonicalization Scheme and capsule_digest, the lowercase-hex SHA-256 over UTF8(JCS(value)) that the Agent Action Capsule profile specifies, so Treeship records can be committed to in a form other implementations recognise. It is additive and nothing signs with it: JCS orders object keys by UTF-16 code unit where Treeship's signing encoding orders by code point, and the two invert above the Basic Multilingual Plane, so changing the existing digest would break verification for every record already in the field. A test pins that divergence from both sides. Floating-point values are refused with an explicit error rather than converted, because the capsule profile forbids them in digest-bearing material and a producer that invents a mapping emits digests nobody else can reproduce; integers outside the IEEE-754 safe range are refused for the same reason. Empty members and nulls participate, as the profile requires, rather than being stripped the way the withdrawn jcs-n construction does.
  • One canonical-JSON encoder instead of three. disclosure, merkle::checkpoint and statements::invitation each carried their own copy of canonical_json_string, and each one feeds a digest that ends up inside a signed artifact. They were the same algorithm written three times, with nothing stopping one from drifting and silently changing a digest that older records were signed over. They now share core::canonical, whose tests pin the encoding byte for byte, pin that empty members and nulls participate rather than being stripped, and pin that keys sort in code-point order. The module documents what this encoding is and is not: RFC 8785 sorts keys by UTF-16 code unit and prints numbers by ECMAScript rules, so adopting it means a new, additive digest, never a change to this one. No behaviour changes.
  • A digest is a commitment, not redaction. TREESHIP.md said that without the original arguments "the digest reveals nothing", and wrap's output-summary comment said a digest "discloses nothing". Neither holds when the committed value is guessable: a digest over a small enumeration, a short identifier or a bounded amount is recovered by hashing the candidates. Both places now say what a digest actually gives you, and point at the enclosing object or the salted selective-disclosure path for low-entropy values. No behaviour changes.
  • The dogfood's verifier pin says why it exists. receipts.yml pinned treeship-version: "0.31.5" with no reason given, and that pin was holding a failure out of sight: the committed fixture was sealed before 0.31.10 by an agent holding its own key, so its close record names no record key, and the receipt_binding check added in 0.31.10 fails it under every newer release. It passes under --structural, but that path does not check signatures at all, so using it here would turn the dogfood into a check that proves nothing about the signing it exists to prove. The pin now carries the reason, the consequence of raising it, and the condition for removing it: reseal the fixture with a current release under the ship key pinned in receipts/trust_roots.json, which only the maintainer holds.
  • The WebAssembly build names its entropy source. p256 0.14 brings getrandom 0.4 in through crypto-bigint 0.7, and getrandom 0.4 refuses to compile for wasm32-unknown-unknown unless a backend is named: that target has no OS entropy source, so the crate makes the caller say where randomness comes from instead of guessing. treeship-core-wasm now declares it, scoped to the wasm target so native builds of that crate do not pull wasm-bindgen in through getrandom. It points at crypto.getRandomValues, the same place the existing getrandom 0.2 js feature already pointed. Three CI jobs failed on this one cause -- the wasm feature matrix, the cross-SDK contract suite (which builds the module with wasm-pack) and docs-drift -- because all three build for wasm.
  • The keystore stopped using deprecated generic-array constructors. The newer sha2 deprecates GenericArray::from_slice and as_slice, and CI runs clippy with -D warnings, so eleven call sites in the AES-256-GCM keystore and the approval-use digests had to move. The key is built from the 32-byte array directly and the nonce through try_into, which turns the length guarantee the framing check already established into an assertion rather than an assumption. The bytes are identical, and rather than take that on faith it was checked across binaries: a keystore written by the previous release decrypts, signs and verifies under this one.
  • The dependency audits pass again, and the one advisory that reached the shipped binary was fixed rather than ignored. cargo audit had accumulated twelve vulnerabilities since the last sweep, because an advisory database moves whether or not the code does. Eleven of them -- five in quinn-proto, four in rustls, one in rustls-webpki, one in crypto-bigint through a second path -- were already fixed upstream and cleared by updating the lockfile. The twelfth, RUSTSEC-2026-0387 (timing variability in operations meant to be constant-time), reached treeship-cli through p256 → elliptic-curve → crypto-bigint 0.5.5, which .cargo/audit.toml says plainly is not a candidate for the ignore list: fix it or hold the release. Fixing it meant p256 0.13 → 0.14, whose crypto-bigint 0.7 has the constant-time work. The API moved in three places -- to_encoded_point became to_sec1_point, and SigningKey::random became the Generate trait, whose generate() draws from the system CSPRNG through the crate's own getrandom path rather than this crate reaching for a second, differently-versioned OsRng. The encodings are unchanged, which the Verifiable Intent interop fixtures pin byte-for-byte and which they still pass.
  • Three dependency audits are green again, and one of them was a real exposure. @treeship/mcp depended on @modelcontextprotocol/sdk ^1.29.0, which resolves inside the range an advisory covers: an OAuth client could send credentials to an authorization server the MCP server chooses (GHSA-6qxp-vccf-f47h, high, affecting 1.12.0 through 1.30.1). The floor moves to ^1.31.0, the first patched release. The Rust audit failed on four advisories in quinn-proto 0.11.16, which does not reach the shipped binary but is in the lockfile; cargo update -p quinn-proto takes it to 0.11.19, above the highest required floor, so nothing had to be added to the ignore list. The Go audit failed on fifteen standard-library findings, all of them in the toolchain rather than in any module we wrote, nine needing Go 1.26.9; the CI pin moves from 1.26.6 to 1.26.9 and the job's comment records that raising the pin is the usual fix for a standard-library finding.

What's Changed

  • chore: refresh npm lockfiles for 0.31.14 by @rezker1 in #568
  • Ship Verifiable Intent as published, with a wallet checkout demo by @rezker1 in #571
  • Write the Verifiable Intent purchase as one post by @rezker1 in #577
  • Record a local HTTP 402 under one budget, and withhold the payment past it by @rezker1 in #581
  • deps: clear the three dependency audits by @rezker1 in #578
  • deps: clear twelve advisories, and fix the one that reached the binary by @rezker1 in #587
  • core: one canonical-JSON encoder instead of three by @rezker1 in #569
  • core: RFC 8785 canonical JSON and the capsule digest by @rezker1 in #572
  • docs: a digest is a commitment, not redaction by @rezker1 in #570
  • report: make the session receipt legible at screen size by @rezker1 in #574
  • session: tell the person where their receipt is by @rezker1 in #575
  • receipts: post the verdict on the pull request by @rezker1 in #576
  • receipts: say why the verifier version is pinned by @rezker1 in #579
  • receipt: carry what the judges said by @rezker1 in #580
  • verify: honour a signed redaction, and say the trace is partial by @rezker1 in #582
  • receipt: attribute the narrative to the evidence by @rezker1 in #583
  • package: redact, a copy of a trace with steps withheld by @rezker1 in #584
  • content: the bytes behind a digest by @rezker1 in #585
  • Release v0.32.0 by @rezker1 in #588

Full Changelog: v0.31.14...v0.32.0