Repository navigation
- 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, andchain_linkagewarns 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 newverified-partial: everything disclosed checks out and the reader is holding part of a trace, which the word now says. treeship package redactwrites 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 theartifact_idthey 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.htmlis 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 asverified-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, andtreeship package content addrefuses 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 lslists what the trace commits to and how much of it is openable here;getrecomputes the hash before it hands anything over and writes to stdout by default, so the bytes can be piped intoshasum -a 256and checked by someone who trusts neither the package nor this binary;dropremoves bytes and keeps the commitment to them. package verifygains acontentrow, 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 nocontent/directory get no row at all.treeship package redactnow 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 owncontentrow. 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.processesin the clear. Matching by timestamp or name would have been a guess, and a redaction that silently misses an entry is worse than none.TimelineEntryandProcessExecutionnow carry an optionalartifact_id,wrapnames 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.v1receipts were sealed into the package and read bypackage verify, but nothing about them reachedreceipt.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 closenow fills ajudgementssection 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-receiptsverified 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 needspull-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 closeprinted the package path only inside apackage verifyhint, never named the human-readable report, and never mentionedtreeship dashboard; the Claude Code plugin's SessionEnd hook announced the sealed session throughadditionalContext, 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 closenow printsreport:with the path topreview.htmland hintstreeship dashboardalongside the verify and publish paths, and the JSON output gains areportfield 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 insystemMessage, the documented user-facing field, whileadditionalContextnow instructs Claude to say it out loud as well, because the hooks reference does not state whethersystemMessagerenders 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::jcsadds the JSON Canonicalization Scheme andcapsule_digest, the lowercase-hex SHA-256 overUTF8(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 withdrawnjcs-nconstruction does. - One canonical-JSON encoder instead of three.
disclosure,merkle::checkpointandstatements::invitationeach carried their own copy ofcanonical_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 sharecore::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.mdsaid that without the original arguments "the digest reveals nothing", andwrap'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.ymlpinnedtreeship-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 thereceipt_bindingcheck 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 inreceipts/trust_roots.json, which only the maintainer holds. - The WebAssembly build names its entropy source.
p256 0.14bringsgetrandom 0.4in throughcrypto-bigint 0.7, andgetrandom 0.4refuses to compile forwasm32-unknown-unknownunless 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-wasmnow declares it, scoped to the wasm target so native builds of that crate do not pullwasm-bindgenin throughgetrandom. It points atcrypto.getRandomValues, the same place the existinggetrandom 0.2jsfeature already pointed. Three CI jobs failed on this one cause -- the wasm feature matrix, the cross-SDK contract suite (which builds the module withwasm-pack) anddocs-drift-- because all three build for wasm. - The keystore stopped using deprecated
generic-arrayconstructors. The newersha2deprecatesGenericArray::from_sliceandas_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 throughtry_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 audithad accumulated twelve vulnerabilities since the last sweep, because an advisory database moves whether or not the code does. Eleven of them -- five inquinn-proto, four inrustls, one inrustls-webpki, one incrypto-bigintthrough 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), reachedtreeship-clithroughp256→elliptic-curve→crypto-bigint 0.5.5, which.cargo/audit.tomlsays plainly is not a candidate for the ignore list: fix it or hold the release. Fixing it meantp256 0.13 → 0.14, whosecrypto-bigint 0.7has the constant-time work. The API moved in three places --to_encoded_pointbecameto_sec1_point, andSigningKey::randombecame theGeneratetrait, whosegenerate()draws from the system CSPRNG through the crate's own getrandom path rather than this crate reaching for a second, differently-versionedOsRng. 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/mcpdepended 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 inquinn-proto0.11.16, which does not reach the shipped binary but is in the lockfile;cargo update -p quinn-prototakes 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