Changelog
Recent Treeship releases, newest first. The complete history remains in the repository changelog.
The latest Treeship releases are documented here.
The format follows Keep a Changelog; versioning follows Semantic Versioning. The canonical source and complete release history live in CHANGELOG.md at the repo root. Edit that file, not this page.
Unreleased
0.21.0 (2026-07-21)
The accountability release. Treeship starts answering not just "was this signed?" but "what was it authorized to do, what did it actually touch, and how confident are we the effect really happened?", with the verdict separating operational confidence from cryptographic validity.
Added
- action/v2 receipt format (verification path). An additive superset of
action.v1: the signed payload now carries a per-hopmandate(the authority an action ran under, grantor, scope, audience, expiry, delegation depth, revocation) and aneffectblock (what the action touched, input/output/ readback hashes, bytes moved, cost, side effects, context snapshot). Fully optional and backward-compatible; existing v1 artifact IDs are unaffected. This release ships the format, the core types, and verification; CLI emission of v2 receipts is a follow-up. effect_confidence, the honest effect verdict ("the ack is not the act"). An actor declares how confident it is that an action's real-world effect happened (verified/partial/ambiguous/unknown/not_verified).verify_effectreconciles that claim against independent, actor-unmintable evidence (an externalreadback, or a witness a trusted authority vouches for): aVerifiedclaim with no such evidence is downgraded, never taken on faith, and the verdict separates operational confidence from cryptographic validity. Honest lesser claims pass through unchanged, the verifier blocks inflation, it does not erase an actor's own hedging.- Runtime identity on action/v2. Optional
provider/model/tool_schema_hash/system_prompt_hash, bound into the signed statement, so a verifier holding a pinned expectation can detect a swapped model, an altered tool set, or a changed system prompt after the fact. - Witness map on effects. Independent corroborating observers per effect. A
witness lifts the confidence ceiling only once a
WitnessAuthorityconfirms its signature against a trusted, non-actor key observing the same post-state; the default authority trusts nothing, so a bundled witness never inflates a verdict on its own. - Unified verifier in the browser and SDKs. Resolution and presentation
verification moved into
treeship-coreand exposed via WASM + the TS/Python SDKs, so browser and edge verification run the same logic as the CLI. - Memory-provenance binding spine. Quarantine-gated approvals (a high-privilege
action's grant is withheld if a memory-provenance check comes back dirty, and the
denial is itself signed), and signed
blocked.v1refusal artifacts that make negative space, what an agent was stopped from doing, and why, first-class evidence.
Changed
treeship trust add --kindaccepts the v0.19 split kindscert_issuer,hub_org, andrevokeralongside the existing ones, closing the pin-exchange gap where a counterparty following the documented flow hit a CLI error.treeship verifysurfaces the action/v2 effect verdict, aruntime:line and aneffect:line on the timeline, and the same verdict in--json, kept clearly distinct from the signature check.
Docs
- Full docs truth-sync. Every documented CLI command, Hub API route, and SDK contract regenerated from code and rewritten to match what v0.20+ actually does, plus CI drift gates (generated command matrix, feature inventory, docs routes) that fail on any code-vs-docs divergence, so a docs lie now requires deliberately overriding a failing check rather than simply forgetting.
0.20.0 (2026-07-12)
The private-verification release. Treeship gets a way for an agent to prove a subset of what it is authorized to do without revealing the rest, and the experimental zero-knowledge path gets rebuilt on an honest foundation.
Added
- Selective capability disclosure —
treeship present --disclose <caps>. An agent card commits to its full capability set, but a holder can present only the capabilities a given verifier needs. Built as an SD-JWT-style disclosure layered over the existing DSSE signature (core:disclose_capabilities/reconstruct_capabilities): the verifier reconstructs the disclosed subset, checks it against the signed commitment, and learns nothing about the withheld entries — while the card's signature still verifies. The withheld set cannot be forged or silently extended, and an omitted capability cannot be presented as disclosed. Round-trip tested end to end.
Changed
- Honest zero-knowledge rebuild (statement-first). The experimental Groth16
path was not sound to ship — it had no trusted-setup ceremony and its proofs were
not bound to the statement being proven, so a proof could be replayed or forged
(identified in the ZK audit). That path is now quarantined, the dead ZK code
deleted, and the docs corrected to stop implying it was authoritative. A
statement-first private-verification design (renamed from
zk-verification) supersedes it and names the two tiers concretely;treeship zk-setupandzk-tls-setupscaffold the SRI-identified building blocks. Thezkfeature remains pre-release and non-authoritative — nothing in the default trust path depends on it.
Docs
- Private-verification spec (statement-first) with the first-principles argument and citations, and a first-draft "cyberlogic" theory of Treeship's trust model.
0.19.1 (2026-07-11)
The portable-verification release. The headline is a small command that closes a
real gap: a Treeship signature is over the DSSE PAE (DSSEv1 <len> <payloadType> <len> <payload>), not the payload JSON, and nothing exported those exact bytes —
so a counterparty verifying a receipt with a third-party Ed25519 library had to
guess the PAE construction and failed. The "portable, offline, don't-trust-us"
promise was true in the bytes but unusable without reading the source.
Added
treeship receipt export <id>— the offline-verifiable triple. Emits the exact{message (the PAE), signature, public_key, algorithm}in copy-safe base64, so a counterparty confirms a receipt with any Ed25519 library and zero Treeship code.--format jsonpipes the triple straight to a partner. The message is reconstructed from the samepayload_type+payloadthe verifier sees, so what is exported is provably what the signature covers; the public key comes from the local keystore (export is for a receipt you produced — a received receipt is checked withtreeship verify).scripts/verify-receipt.py— the reference verifier. Dependency-light, independent Ed25519, offline:treeship receipt export <id> --format json | python3 scripts/verify-receipt.py→VALID. The canonical thing a partner copies.- Receipt format reference. The stable, partner-facing description of the byte format — envelope, PAE, id derivation, a language-agnostic verification recipe — so anyone can implement a verifier without reading the Rust. The format is frozen; this documents it.
Fixed
- A clean session reports
verification_status: "pass", not"warn"(#231). The always-onreceipt_body_bindingscope caveat added in 0.19.0 is informational (every package carries it), but it was flipping every cryptographically-clean session's top-line verdict towarn, makingpassunreachable and breaking any caller that gates onstatus == "pass". It is now surfaced inwarningsfor transparency but no longer downgrades the verdict; real warnings (git-degraded, event-log skips, determinism drift) still do. Caught by the post-publish smoke test; regression-tested. - Release tooling: npm pinned to 11.5.1 (#230). The 0.19.0 publish failed on
npm because
npm@lateston the runner's Node 24 shipped a build whose bundledsigstoremodule was unresolvable, crashing trusted-publishing provenance (crates.io and PyPI published fine). npm is now pinned to the trusted-publishing floor instead of tracking@latest. - Dead verify URL in the lobster-cash demo. It printed the non-resolving
hub.treeship.dev/verify/; the public verify URL ishttps://treeship.dev/verify/<id>(the hub's ownhub_url).
0.19.0 (2026-07-09)
The security-hardening release. Two independent AI-assisted adversarial audits
were run against the codebase, and every confirmed finding that ships in the
default binary is fixed, each with a regression test that fails before the
fix. Highlights below; the recurring theme both audits named was "a higher-level
surface reporting verified / authentic / pass from attacker-controlled
input without anchoring to a verified signature," and that class is now closed
across the CLI, the WASM browser verifier, the hub, the SDKs, and the bridges.
Changed (breaking)
- The
shiptrust-root kind is split intohub_org/cert_issuer/revoker. A single--kind shippin used to authorize three unrelated powers at once — promoting a local journal claim to a global single-use claim (hub-org checkpoints), issuing agent certificates, and revoking capabilities — so pinning a hub for dedup silently also let it mint certs and kill your capabilities. Each verifier is now scoped to the one power it needs. Hard cutover: no verifier honorsshipany more, andtreeship trust add --kind shipis rejected with guidance to the three new kinds. Existingtrust.jsonfiles still load (the deprecatedshipkind stays parseable but inert); a legacyshippin is inert until re-pinned. Nothing published breaks — the trust kind is a local, verifier-side setting, not stored on the hub or embedded in any artifact.keys exportnow emits the new kinds. Migration:treeship trust add <key_id> <pubkey> --kind cert_issuer(orhub_org/revoker).
Security
treeship verifyon a fetched receipt/URL/package no longer reports "authentic" without checking a signature. These paths ran only keyless, self-referential checks (recomputed Merkle root vs the receipt's own root, inclusion proofs, leaf-count, timeline) and then printed "Verified. This receipt is authentic." (JSONoutcome: "pass"). Any internally-consistent forgery, or an empty receipt, passed. Verification now says "Structurally consistent" with an explicit warning that signatures and issuer were NOT verified, the JSONoutcomeisstructural-pass(neverpass) withsignatures_verified: false, and an empty receipt is afail. The same fix lands on the WASMverify_receiptsurface.- The hub DPoP signing key is encrypted at rest. It was written to
config.jsonas plaintext hex, so a passive read (backup, dotfile sync, container layer, a committed.treeship, a co-tenant) handed an attacker full impersonation of your ship to the hub. It is now sealed under the machine key with the same AES-256-GCM path the ship key already uses — a stolenconfig.jsonis useless on another machine. - The browser/WASM
verify_capabilitynow actually verifies signatures. It reported a card as key-bound / "verified" from the unverifiedsignatures[0].keyidstring (a public label, not a secret), so a forged card naming your pinned key could show as cryptographically bound to you in a receipt viewer. It now builds a real verifier from the pinned trust roots and requires the card's key to have produced a valid signature — matching the native CLI by construction. - The hub verifies checkpoint signatures and binds each signer to its first public key.
PublishCheckpointstored signer/signature/public key with no Ed25519 verification, which let an attacker mint the "ownership" signal used to shadow another tenant's consistency chain. The CLI now sends the exact signed canonical bytes; the hub verifies them and cross-checks the stored fields, plus a trust-on-first-usesigner → public_keybinding. - At-rest keystore keys derive from a secret, not from guessable machine identifiers. The AES key wrapping every signing key was
SHA256(machine-id/serial/hostname ‖ path)— all guessable — so an exfiltratedkeys/*.jsonwas forgeable by anyone who knew your hostname. It now derives from a 32-byte OS-CSPRNGmachine_seed(mode 0600, the primary entropy on every platform), with the machine id mixed in only as a binding salt; the old derivations remain as decrypt-only migration fallbacks that re-wrap on first open. Two hosts with identical machine-id/hostname but different seeds can no longer decrypt each other's keystore. The seed file is also hardened against planted symlinks / loose permissions. - Ed25519 verification is strict across the whole core, and
treeship verifycryptographically checks chain linkage. Every core verifier now usesverify_strict(rejecting signature malleability, small-order keys, and cross-SDK split-view; previously onlypresent.rsdid). And the chain walk now cross-checks each child's signedparentIdagainst the walked parent instead of trusting the unsignedparent_idstorage metadata, exiting non-zero withSIGNED LINKAGE BROKENon a mismatch rather than printing "no tampering detected" for a check it never ran. verify-capability: three ways it could be fooled, all closed.meta.toolcan no longer rescue a concrete out-of-scope action into scope; key-boundness / the action cross-check / revocation authorization now use the re-verified signer rather than the unverifiedsignatures[0].keyid; and provenance grades read off a card that is not key-bound are markedSELF-ASSERTED (not machine-verified). The Layer-4 authorization pass additionally hardened invitation countersigning (re-validates the joiner's statement before the host signs),capability revoke(refuses to print success for a revocation no verifier will honor), and scope checking (fails closed onextraconstraints it cannot evaluate).- The trust-ladder can no longer be laundered. A self-declared
attestation_classon a work-history record was tallied verbatim, so a hand-signedsession.v1withattestation_class: "countersigned"laundered into the highest trust bucket.session.v1can no longer be minted through the generic attest path, the predicate validator now enforcesenum/const, andprofile/historycap the declared class to what the record's own evidence supports. - Hub object-level authorization and availability. Publishing a Merkle proof or consistency row now requires the caller to own the artifact / checkpoint / signer (was any authenticated dock, enabling cross-tenant shadowing). The hub gained request timeouts (Slowloris protection), a timeout + response cap on its Rekor call (no full-hub freeze during a Rekor outage), and a body cap on the unauthenticated
/dock/authorize.session closenow stamps areconcile_degradedmarker when the git backstop was disabled mid-session, andpackage verifywarns on it, instead of sealing an incomplete file ledger as clean. - CLI security nonces use the OS CSPRNG, and the A2A bridge stops trusting unverified JSON. Every key/nonce/token in the CLI (hub keypair, DPoP jti, approval and invitation nonces) moved from
thread_rngtoOsRng, with sub-128-bit nonces widened to 128. The A2AverifyReceiptno longer returnsshipId/withinDeclaredBoundscomputed from the raw fetched JSON when the receipt did not verify (and no longer defaultswithinDeclaredBoundstotruewhen there is no declaration). - The Claude Code / Kimi plugins redact secrets from captured commands. Bash commands were recorded verbatim into the session timeline, which is publishable to a no-auth URL, so
AWS_SECRET_ACCESS_KEY=… cmd/Bearer …/--token=…leaked into shareable receipts. Env-assignment secrets, secret CLI flags, and bearer tokens are now redacted before capture. - Lower-severity hardening: stored artifact
key_idis taken from the signature that actually verified (notsignatures.first());format_device_codeno longer panics on a multibyte device code; an unreadable file's digest is recorded asnullrather than an empty string; the SDK↔WASM trust-root ABI is fixed; and the event-log read no longer aborts the whole session on one bad byte, nor doespackage verifyimply it authenticated the unsigned session narrative. zk-feature (pre-release) soundness: the RISC0 chain guest now fails closed on empty content/signatures (was scoring an empty chain as validly signed), the hand-rolled base64 in the signature path is replaced with a real decoder, and the Groth16 verifier output no longer echoes an unverifiedartifact_idas bound. Thezkfeature remains pre-release and non-authoritative.
Added
treeship match --exercised <glob>+GET /v1/agents/match— find agents by exercised evidence (work-history slice 4). Declared capability gets an agent found; exercised history gets it chosen. The Hub indexestools_exercisedfrom the signedsession.v1records it already holds and proposes candidates (filterable by--class/--min-sessions); the client re-verifies every candidate on this machine and re-ranks by verified sessions, showing records that do not verify asunverifiedrather than trusting the Hub's own match. See the work-history spec.GET /v1/stats— public adoption metrics. Counts only, never identifiers: artifacts pushed (total / last 7d), docks attached vs active (active = pushed in the window), session receipts uploaded, distinct agents seen. Any query failure returns 500, never a partial zeros document — zeros on error would read as "no adoption", a lie in the dangerous direction. Verify-page traffic and package downloads deliberately stay in external systems; the hub reports only what its own tables can answer, so every number is checkable against the same database the transparency endpoints serve.
Changed
treeship --helpnow reads like a protocol, not a platform. The core loop (quickstart → init → add → wrap → attest → session → verify → hub → publish) surfaces first; 21 extension and experimental commands (ui, dashboard, templates, otel, daemon, merkle, bundle, the zk family, …) are hidden from default help but unchanged in behavior.treeship help --alllists everything.- One site list now stamps AND checks every release-version site.
check-release-versions.py --write <version>inverts every parser into a writer; release.sh's parallel sed/npm/node bump list is deleted. Forgetting one side of the dual list is the drift class behind the 0.9.6, 0.10.2, and 0.17.0 misses. The four npmpackage-lock.jsonfiles become first-class checked sites (26 → 30). - CI now runs the full shipped-binary test surface (
treeship-cli,treeship-core-wasm, the hub's Go tests, and the plugin hook parity suite), not justtreeship-core— so a fix's regression test actually gates merges on push.
0.18.0 (2026-07-07)
Added
treeship profile/verify-profile— the checkpoint-pinned track record (work-history slice 3). A profile ("N sessions, N actions, these tools, this span") is a claim, and claims areasserteduntil checked. Every number is a deterministic aggregation over the log's firsttree_sizeleaves at a pinned checkpoint whose root travels in the payload;verify-profilerebuilds exactly that prefix, cross-checks its root against the pin, recomputes every field through the same shared aggregation path as compute (the two cannot drift), and compares — match grades the profilechecked; mismatch is a provable lie, named field by field (nonzero exit, structured JSON).--attestsigns the profile as a ship claim (profile.v1, new registered predicate — the operator's claim about the agent's record, never the agent grading itself). Demonstrated live: an attested profile re-verifiescheckedeven after the log grows, because the pin freezes the prefix. Reputation pinned to a root is falsifiable; reputation that floats is marketing. See the work-history spec.treeship history <agent>+GET /v1/agents/history— the work-history projection (work-history slice 2). An agent's work history is its transparency log filtered to signedsession.v1records — no new store, no new trust surface. The Hub serves raw signed envelopes plus Merkle anchors (it filters and serves, never interprets); the CLI re-verifies every envelope on your machine against your own trust roots, re-proves each anchored entry's Merkle inclusion offline, and reads the typed fields from the verified envelope — never from transport metadata. Sortable and filterable on schema fields (--class self|runtime|countersigned,--since <rfc3339>,--limit), newest first;--format jsonemits one structured object, and a served entry whose inclusion proof fails flips the exit code nonzero, per the honest-verdicts rule. The output states the honest bound on every run: history proves what was recorded, never everything that happened. See the work-history spec.
Fixed
statusnow shows WHICH store it resolved. Store discovery walks up from the working directory and silently switches between project-local and global.treeshipconfig — previously visible only indoctor, and the root cause of a whole class of wrong-keystore confusion (it struck three separate times in one working day, including twice against the maintainers).statusprintsstore: project-local -- <path>in text mode and astoreobject in JSON.- A failed append-only proof no longer advances the audit witness. The witness is the monitor's memory of the last state the hub proved; previously it advanced on any monotonic growth, so a hub that could not prove its log only appended (possible rewrite) triggered the alarm exactly once and every subsequent audit compared against the new, unproven baseline and reported clean. The witness now persists only when neither equivocation nor an append-only failure contradicted it — the alarm keeps firing until the hub actually proves the extension, and the output says
witness NOT advanced (extension unproven).
Fixed
- Hub hardening (audit Batch G). Four fixes and a test wall for the network layer: (1) request bodies on
POST /v1/artifactsand allPOST /v1/merkle/*are now capped at 10 MB (the receipts handler always was; the other four buffered unbounded uploads into memory). (2) New indexesartifacts(payload_type, signed_at)andartifacts(dock_id)— the public, unauthenticatedGET /v1/agentsand/v1/agents/logendpoints were full-table scans that decode every receipt row per request and only get slower as the log grows. (3)PRAGMA foreign_keys=ON: everyREFERENCESclause in the schema was decorative (SQLite ignores them unless enabled); a write claiming a dock the hub never registered is now refused by the database itself. (4) Re-POSTing a checkpoint no longer inserts a duplicate row forever —InsertCheckpointis idempotent on its natural key (signer, root, tree_size) and returns the original row's id, matching the artifact/consistency insert discipline. (5) The publicGET /v1/verify/{id}no longer echoes the verifier's stderr and internal error strings to anonymous callers (logged server-side; the response is a generic verdict — anyone wanting detail runstreeship verifythemselves). And the DPoP authentication boundary — through which every authenticated write funnels — now has a test suite (it had zero): the accept path plus jti replay, foreign-key signature, unknown dock, htm/htu binding mismatches, clock skew, alg confusion, malformed JWTs, and payload tampering under a stale signature are all pinned.
0.17.1 (2026-07-06)
Fixed
- The version train now covers the plugin manifests (the 0.9.5-plugin bug). The Claude Code plugin's own
plugin.jsonadvertised 0.9.5 — eight releases stale — because it was covered by neitherrelease.shnor the version guard (the sibling marketplace manifest was; the plugin's own manifest was not). Same class: the OpenClaw plugin manifests sat at 0.10.3. All three are bumped,release.sh preparenow bumps them, and the preflight guard walks them (23 → 26 sites), so this class of drift fails the release instead of shipping. Also swept in the same pass:PLATFORM.md/TREESHIP.mdno longer hardcode a "current version" in prose (one claimed "all packages are at v0.9.5"); the README's roadmap block ("v0.10.1 in flight", fully shipped long ago) is replaced with a pointer to the livingvision.md; the report skill'streeship package exportinstruction (a command that does not exist) now describes the real portable-package flow; the Kimi skill'scargo install treeship-cli(an orphaned 0.4.0 stub the rest of the docs explicitly warn against) now saysnpm install -g treeship; and stale@v0.10.0skill-pin examples are current. Found by a user with the repo checked out; generalized by the audit. - Merkle inclusion proofs are now generated from the checkpoint's tree, not the current one.
merkle publishandmerkle proofcomputed authentication paths over the FULL current tree while pairing them with the latest checkpoint — but a path is a function of the total leaf count, so whenever any artifact was appended after checkpointing (a completely normal workflow), every emitted proof reconstructed the wrong root and the hub served proofs that verifiably fail, making legitimate, in-log artifacts readinclusion INVALIDto every auditor. Both commands now rebuild the tree truncated to exactly the checkpoint'stree_sizeand cross-check the rebuilt root against the checkpoint's before emitting anything (the same rulemerkle publish's consistency proofs andpresentalready applied), andmerkle proofgains the missing membership guard: an artifact newer than the checkpoint is refused with the fix (treeship checkpoint) instead of handed a proof that cannot verify. Verified live both ways: with the store grown past the checkpoint, an old artifact's proof verifies offline against the checkpoint root, and a too-new artifact is refused. Also fixed in the same class: the receipt verifier reported0/0 inclusion proofs passedas a green check — a check that ran nothing now reports a warning, not a pass. hub attachprobes the hub before claiming "reconnected". With stored keys, attach previously set the active connection and reported success without ever contacting the server — so keys that outlived the hub's dock registration (e.g. after a hub database reset) produced a confident "reconnected" followed by 401s on every push. Reconnect now makes one authenticated, read-only DPoP request first; if the stored keys no longer authenticate, it says so plainly and falls through to a fresh device flow instead of lying. Found live during the hub-recovery incident, confirmed by the audit.- A2A handoff attestation actually records handoffs now. The
@treeship/a2amiddleware invokedattest handoffwith flags the CLI does not have (--task-id,--context,--a2a-message-id) and omitted the required--artifacts, so every handoff attestation failed — silently, because bridge errors are deliberately swallowed. A2A delegation boundaries were never recorded. The bridge now does the honest two-step: it first attests the delegation itself as an action signed by the sender (a2a.delegate, with the task/context/message ids inmeta), then records the handoff transferring that attested artifact — both real CLI primitives, no phantom flags. Found by the full-codebase audit. - The MCP bridge no longer introduces itself as 0.10.0. The handshake version clients see was hardcoded seven releases stale; it now reads from the package manifest and rides the release train.
- Verification commands no longer exit 0 on hostile verdicts, and emit real JSON.
resolve,audit, andverify-capabilitycomputed hostile verdicts — a REVOKED card, out-of-scope violations, an OMISSION against the committed anchor, an invalid inclusion proof, an equivocating checkpoint, a failed append-only proof — printed a warning, and returned success, so any script, CI gate, or monitor keying off the exit code treated a detected history rewrite as green. All four verification surfaces (includingverify-presentation) now exit nonzero on a hostile verdict.audit --watchdeliberately keeps looping and alerting instead of exiting — a monitor's job is to keep watching. In JSON mode (--format json) each of the four now emits one structured verdict object carrying every field the text output shows; previouslyresolve/verify-capabilityreturned a bare success envelope,verify-presentationreturned essentially nothing (its verdict lines went through info, which JSON mode suppresses), andauditstreamed multiple concatenated JSON objects no parser could consume. Programmatic callers — the bridges, the gateway, CI — can finally gate on these commands.
0.17.0 (2026-07-06)
Added
- Challenge mode — the handshake (registry-topology slice 3). A static presentation proves the record; challenge mode proves the bearer. The verifier mints a nonce; the agent answers with
present --challenge <nonce>, signing a domain-separated canonical that binds the agent URI, the exact card, the nonce, and a bearer-signed timestamp (every variable field digest-folded, so no field can inject separators and shift the others); the verifier runsverify-presentation --challenge <the same nonce>and the response is checked only against the subject key the card verification itself established (chain verdict or the verifier's own AgentCert pin — never a key the wire supplied). Verdict:challenge: verified — bearer controls <key> (response 2s old), statusverified (key-bound, anchored, live). Fail-closed everywhere: signing requires the agent's own key and refuses the ship-key fallback (a ship signature would prove the operator, not the agent); a card/key drift refuses to answer; an unverified card means there is no key to check against and the challenge fails rather than "verifying" against nothing. Adversarially exercised live and in tests: a captured response replayed against a new nonce, a response signed by a non-card key, a response replayed for a different card or agent, and a tampered timestamp all reject with specific reasons. treeship present/verify-presentation— agent verification with no registry in the loop (registry-topology slice 2). In real TLS the server hands you its certificate chain;presentgives agents the same move: one file carrying the current capability card, the certificate chain to its ship, any known revocations referencing the card (included, never filtered — the verifier judges their authority), and a Merkle staple — the latest checkpoint plus the card's inclusion proof, computed over the tree truncated to exactly the checkpoint's size with the root cross-checked before anything is written.verify-presentationre-verifies everything fully offline against the verifier's own trust roots: direct leaf pin or certificate-chain walk to a pinned ship root (the same code path asresolve --hub), authorized revocations honored, staple checkpoint signature + inclusion re-checked, and freshness reported as an explicit bound —revocation: none included — current as of the staple (9m old)— with--max-staple-age 30s|15m|2h|1dturning the bound into an enforced verdict (STALE). Honest constraint printed in both commands: a static presentation is replayable — it proves the record, not the bearer; challenge mode (nonce signing, slice 3) is what proves live key control. Adversarially exercised: a card forged to grant itself an extra capability reportsUNVERIFIED(stale signature caught by the chain's subject-key check), an unparseable envelope fails hard, and a verifier with no pins gets actionable pin commands, not false verdicts.
Added
-
treeship onboard— an agent goes from nothing to verifiable in one command. Composes the existing lifecycle —agent register --own-key,attest card(--from-harness/--tools-json/--from-a2a/--tools), and with--publishthe fullpublish+merkle checkpoint+merkle publishanchor — into one idempotent pass, and ends by printing the trust bundle: the exacttrust addcommand(s) a counterparty runs (agent_certfor the agent key,hub_checkpointfor the ship key) plus theresolve/auditcommands that verify the agent. One name in, one URI out:onboard deployerandonboard agent://deployerare the same call, closing the register-takes-a-name / everything-else-takes-a-URI trap. Adds no new attestation surface — every artifact is minted by the same code paths as the individual commands. Born directly from the 13-item friction ledger of a real end-to-end dogfood run. -
Certificate-chain delivery: pin the ship, verify its agents (registry-topology slice 1).
agent register --own-keynow also mints the protocol-native certificate — a typedagent_cert.v1receipt whose envelope signature by the ship key bindsagent://<name>to its per-agent public key (idempotent per agent+key; the.agentpackage cert remains the human-portable rendering).treeship publishpushes the cert chain with the resolvable set, the Hub serves it verbatim in a newcertsfield, andresolve --hubwalks the chain when the leaf key is not directly pinned: cert envelope verified against a pinnedShiproot (the pubkey always comes from your trust store, never the wire), agent URI + subject key matched, validity window enforced fail-closed, and the certified subject key must itself verify the card. Verdicts readverified (chain to pinned ship root)/yes (AgentCert via ship chain). This is the TLS chain shape: a counterparty pins one ship key and verifies every agent under it, instead of pinning each leaf — the gap found in the end-to-end dogfood. Chain-certified keys also count for self-revocations served in the bundle. Adversarially tested: unpinned ship, wrong trust-root kind, expired / not-yet-valid / missing-window certs, subject and agent-URI mismatches, stolen-cert-wrong-signer, and keyid/signer mismatch all reject.
Fixed
- Two agents in one workspace no longer collapse into one card (actor→key mapping destroyed).
derive_agent_idhashed(surface, host, workspace)— the agent's name was not part of its own identity — so registering a second agent from the same workspace reused the first one's card id and the upsert silently overwrote it: the clobbered agent's actor→key mapping (registered_key_for_actor, the lookup per-actor signing resolves through) vanished, downgrading its future receipts to self-asserted and orphaning its certificate. Found live whenfable-scout's registration destroyedfable-code's. The name is now part of the id hash, andregistered_key_for_actorpicks the newest matching card deterministically (filesystem iteration order must never decide which key an actor signs with). Existing cards keep their on-disk ids; a re-register after this fix creates a correctly-keyed card alongside any stale one. - Hub: re-pushing an already-published artifact no longer 500s.
InsertArtifactwas a bareINSERT, so re-publishing an agent's resolvable set (exactly what an idempotentonboard --publishon every agent boot does) hit theartifact_idprimary key and surfaced as a 500 to the pushing client, aborting the publish. Artifacts are content-addressed, signed envelopes — a re-push of the same id is the same bytes — so the insert is nowON CONFLICT(artifact_id) DO NOTHING: idempotent, and deliberately never an update, so a colliding id can never overwrite bytes the hub already serves. Regression-tested both ways (duplicate no-op + overwrite rejection).
0.16.0 (2026-07-06)
Added
treeship keys export— hand a counterparty your public key. Prints a key's PUBLIC half in the pinnableed25519:<base64url>form together with the exacttreeship trust addcommand(s) the counterparty runs:--kind agent_certfor a per-agent key (--agent agent://deployer),--kind ship+--kind hub_checkpointfor the ship key — a remote verifier needs both to getverifiedon resolve andanchored & verifiedon the transparency check. This is the out-of-band half of the trust model: every remote verification is checked against the verifier's own trust roots, and until now there was no sanctioned command that produced the key material those roots need. The private key never leaves the store.--format jsonfor scripts.session.v1— every closed session becomes a typed, signed work-history record (work-history slice 1).treeship session closenow mints asession.v1receipt into the agent's chain alongside the sealed package: headline, outcome, duration,tools_exercised(computed from captured tool usage, never hand-written), counts, and areceipt_digestbinding the record to the exact sealed evidence it summarizes. Each record carries an attestation class from the work-history ladder —self(the agent's own receipts),runtime(a harness hook or bridge the agent cannot forge emitted the log),countersigned(a second party signed evidence embedded in the package, e.g. consumed human approvals) — computed from capture-path evidence actually present, labeled, never laundered (anchoredis a later, derivable state). The record is validated against the registeredsession.v1predicate schema before signing (fail-closed), signed with the actor's own key when it has one (key-bound history for registered agents), and chained to the session-close artifact. Best-effort: a record failure warns and never wedges the close — the sealed package remains the source of truth. This is the atom of the work-history spec: identity says who the agent is, capability says what it claims, work history says what it has demonstrably done.
Fixed
resolveno longer labels an unpinned checkpoint signer "signature INVALID". Checkpoint verification fails closed both when the signature is genuinely bad and when the signer simply is not pinned underhub_checkpointin your trust roots — but those demand opposite reactions (distrust the hub vs pin a root), and reporting the second as "INVALID" is a mislabel.resolvenow distinguishes them: an unpinned signer reportscheckpoint signer not in your trust rootswith the ready-to-runtrust add … --kind hub_checkpointcommand (the pubkey comes from the served checkpoint; pinning remains your explicit decision, and verification still happens on your machine), while a pinned signer whose signature fails still reportsINVALID.- Keystore survives macOS hostname renames (self-healing machine key). The machine key wrapping on-disk private keys was derived from
hostname+ username, and macOS silently renameskern.hostname(network name collisions, DHCP), after which the keystore failed with a misleadingMAC verification failed — wrong machineand the documented recovery was to abandon it. The keystore's own TODO predicted exactly this; it is now implemented. The primary machine key is derived from a hardware identifier (/etc/machine-idon Linux,IOPlatformSerialNumberon macOS), which survives hostname and network changes; machines with neither identifier keep the v1 derivation with its co-located seed file, preserving project-local keystore isolation. Decryption tries an ordered fallback chain covering every wrapping an existing keystore may carry — the v1 key under the current hostname, the raw-path key (pre-canonicalization symlinks), and on macOS the v1 key underscutil LocalHostNamevariants, which typically retain the name the store was written under afterkern.hostnamedrifts away. Any entry that decrypts via a fallback is transparently rewrapped under the primary (same locked, idempotent migration path as the v1-format upgrade), so the fix both recovers already-drifted keystores with no user action and immunizes them against the next rename. Verified against a real keystore bricked by three successive hostname renames: it self-healed on first use. Note: entries rewrapped under the hardware key are not decryptable by older Treeship binaries; keep one CLI version per machine. - Capability matching now understands harness-captured glob patterns (
Bash(git:*)).tool_matchesonly expanded a trailing*, but the patternsattest card --from-harnesscaptures from a Claude Codesettings.jsoncarry the wildcard inside the permission scope,Bash(git:*),Read(*), so every such pattern silently degraded to an exact string match that could never fire, and a card captured from a real config reported every real action out-of-scope. The wildcard may now sit anywhere in the pattern (one*, prefix and suffix must both hold), soBash(git:*)matchesBash(git:status)and still rejectsBash(gh:pr); the falsifiable cross-check keeps flagging genuinely off-card actions (payments.charge). Trailing-glob (family.*), bare-*, and exact matching are unchanged. Sharedtreeship_core::capabilityfix, so the CLI and the WASM browser verifier keep returning the same verdict. Found by dogfooding the full TLS-for-agents flow end to end.
0.15.0 (2026-06-26)
Added
- MCP bridge emits provable receipts by default (protocol-integration slice 1). On startup the
@treeship/mcpbridge provisions a per-agent signing key for its actor (agent register --own-key --quiet), so theattest action --actor <agent>calls it already makes sign with that agent's own key. Receipts now verify asactor proof: proven (key-bound)instead ofasserted, with no change to the attestation path, per-actor signing already did the work once the key exists. Best-effort and idempotent: iftreeshipis missing or uninitialized the bridge still runs (receipts fall back to the shared key, with a one-line note), and restarts reuse the same key rather than minting new ones. The first step of wiring the shipped identity stack into the protocols real agents already speak. See protocol-integration spec. treeship agent register --own-keyis now idempotent, and a new--quietflag skips the on-disk.agentpackage. Re-registering an agent that already has a per-agent key reuses that key (no key pile-up, no duplicate AgentCert pins) instead of minting a second one, and--quietproduces the card + key + pin without dropping a.agentdirectory into the working directory. Both exist so programmatic callers (the MCP/A2A bridges, which register on every startup) can provision identity safely and quietly; interactiveregisteris unchanged.treeship attest card --tools-json <path>— operator-declared capability sets (protocol-integration slice 2). The runtime companion to--from-harness: where--from-harnesscaptures tools observed in a config (gradedcaptured),--tools-jsonrecords an operator's explicit declaration from a JSON file (a["tool", ...]array, or{ "tools": [...] }). Each entry is stampeddeclaredincapability_provenancewith the file as itssource, an operator's claim, honestly labeled and traceable to where it came from, never presented as captured or discovered. A capability already captured from a harness keeps the strongercapturedgrade. This is the deliberate slice-2 choice: the@treeship/mcpbridge exposes Treeship's own meta-tools (treeship_verify, ...), not the agent's domain tools, so auto-capturing from the bridge would be the attestation layer attesting to its own presence. A card's value is the signature binding a claim to an identity and key; an operator declaration is honest precisely when it is labeled as one. See protocol-integration spec.treeship attest card --from-a2a <AgentCard.json>+ A2A bridge provable receipts (protocol-integration slice 3). Maps an A2A agent's own publishedAgentCard.skillstoagent_card.v1capabilities, stamped with a newdiscoveredprovenance grade (read from the agent's own descriptor) and the AgentCard'surlassource.discoveredis a real grade, distinct fromcaptured(harness) anddeclared(operator), weaker than receipt-exercised, andverify-capability/resolvecount it in its own bucket so A2A skills are never mislabeled as operator-declared. Protocol-levelcapabilities(streaming, push) are excluded, transport, not domain capabilities. Unlike the MCP meta-tools dead end, an A2A AgentCard carries the agent's own skills, so it is a legitimate source. Separately, the@treeship/a2amiddleware now provisions its actor's per-agent key on construction (provisionAgentKey, idempotent + best-effort), the slice-1 treatment for A2A, so its task and handoff receipts verify asproven (key-bound). Net: an A2A agent's self-description becomes a key-bound, verifiable Treeship card, and its receipts are provably the agent's.treeship auditnow witnesses checkpoints across runs (transparency-log slice 3a). Each audit persists the highest verified checkpoint it sees per(hub, signer)under~/.treeship/merkle/witnessed/, and compares the current one against it: aconsistency:line reports monotonic growth, and audit warns on a forking or regressing hub, two different roots at the sametree_size(equivocation), or atree_sizethat went backwards. This is gossip/witnessing: it catches a log that contradicts itself, the "monitors catch anomalies" property. Honest framing throughout: witnessing does not cryptographically prove append-only (a later tree extends an earlier one), that is the consistency proof, slice 3b, whose design (publish-side generation via the shipped Merkle consistency primitive, since the Hub holds no tree of its own) is now captured in the transparency-log spec.- Cryptographic append-only proof for the transparency log (slice 3b).
treeship auditnow proves a hub's log was only appended to (no silent rewrite) since you last witnessed it, the true Certificate-Transparency property. Because the Hub holds no Merkle tree ("the Hub creates nothing"), the proof is generated publisher-side:treeship merkle publishcomputes a Merkle consistency proof from the previous checkpoint to the current one (over the tree truncated to exactly the checkpoint size, with the truncated root cross-checked against the checkpoint's own root before anything is pushed) and POSTs it; the Hub stores and serves it verbatim viaGET /v1/merkle/consistency.auditfetches the chain and re-verifies every link offline against the shippedverify_consistencyprimitive, enforcing four properties so a malicious hub cannot pass a valid-but-irrelevant chain: it must start at the witnessed checkpoint, every link's proof must verify, links must be contiguous, and it must end at the current checkpoint. The audit reportsappend-only VERIFIED — current tree cryptographically extends tree_size N, or warns of a possible history rewrite if a served chain fails. Generation↔verification agreement and tamper rejection (wrong start, gap, short endpoint, wrong head, mutated proof) are unit-tested against real trees. See the transparency-log spec.
Fixed
- Keystore: canonicalize the store path before deriving the machine key. The machine key that wraps on-disk private keys hashes the keystore directory path, so the same directory referenced by two different path strings (a symlink in the path: macOS
/var→/private/var, or a symlinked$HOME) derived two different keys and failed to decrypt with a misleadingMAC verification failed — wrong machineerror on a perfectly valid keystore.Store::opennow canonicalizes the path first, so one directory always yields one key. The raw-path key is retained as a decrypt-only fallback, so any keystore written before this change still opens, this hardening never locks an existing user out. Regression-tested both ways (symlinked-path consistency + legacy raw-path fallback).
0.14.0 (2026-06-25)
Added
-
Merkle consistency proofs (
core, transparency-log slice 3 foundation).MerkleTree::consistency_proof(old_size)andverify_consistency(version, old_size, old_root, new_size, new_root, proof): prove (and verify offline) that a later tree extends an earlier one, the firstmleaves unchanged, only appends. This is the append-only / no-rewrite guarantee for the transparency log. Derived and validated for this tree's exact level-by-level-promotion shape: round-trip tested againstcompute_root(ground truth) for every(m, n)up to 20 across both v1 and v2, plus tamper rejection (wrong roots, mutated proof) and edge cases. See merkle-consistency spec. -
treeship audit --watch— transparency-log monitor mode. Re-audits an agent's history on an interval (--interval, default 30s) and keeps alerting on omission, the "monitors catch anomalies" piece of the transparency log. Transparency-log slice 4. -
treeship audit --hub <url> <agent>— transparency-log audit. Pulls an agent's history from a Hub, re-verifies each anchored entry's Merkle inclusion offline against your trust roots, and checks completeness against the agent's committedevidence_anchor, so omission is detectable (OMISSION — committed 5, observed 3). The client half of Certificate Transparency for agents: the Hub serves metadata + anchors, your machine decides. Transparency-log slices 1b + 2. -
Hub agent transparency log (
GET /v1/agents/log?agent=<uri>). An agent's append-only receipt history, its actions and cards as metadata + Merkle anchors only, never payloads, newest-first, plus the latestcommitted_anchor(the card'sevidence_anchor). The server half of Certificate Transparency for agents; a client (treeship audit, next) re-verifies each anchored entry's inclusion and checks completeness against the committed anchor. Slice 1 of the transparency-log spec. -
treeship publish <agent>. Pushes an agent's resolvable set, its current capability card and any revocations, to the configured Hub through the authenticatedhub pushpath, so anyone canresolve --hubit over the network. Completes the agent resolver (DNS + OCSP + Certificate Transparency for agents): local + remote resolution, revocation, and a Merkle transparency anchor, all re-verified client-side. New Agent Resolution and capability provenance docs. -
Transparency anchor in agent resolution.
GET /v1/agentsnow includes the current card's Merkle inclusion proof when it has been anchored, andtreeship resolve --hubre-verifies it offline (the checkpoint signature against your trust roots + inclusion in the signed root), reportingtransparency: anchored & verified (checkpoint #N)ornot anchored. Proves a resolved card is in the log, not just signed, the Certificate-Transparency property for agents. Slice 2 of the agent resolver. -
Hub agent resolver endpoint (
GET /v1/agents?agent=<uri>). Resolves an agent URI to its verifiable bundle, the current capability card and any revocations, returned as raw signed envelopes. The Hub performs no verification or authorization here; it serves bytes, and the client (treeship resolve) re-verifies and grades them offline, so the Hub and the CLI / WASM verifier cannot drift. Slice 1b of the agent resolver (the network half). Seeagent-resolverspec. -
treeship resolve --hub <url>— network resolution end to end. Resolves an agent from a Hub over the network and re-verifies the returned card and revocations client-side against your own trust roots (an Ed25519 check; the Hub's word is never trusted). Reportssignature: verified | UNVERIFIED,key-bound, revocation status, and the card's captured-capability mix. Theexercisedgrade is local-only (the bundle carries no action receipts). This connects the resolver's two halves: an agent URI now resolves to a re-verifiable bundle from anywhere, the "DNS for agents" lookup. Slice 1c of the agent resolver. -
Capability provenance: captured vs exercised vs declared.
verify-capabilitynow grades each declared capability by its provenance —captured(read off the card's optionalcapability_provenanceblock, set from harness config at mint time),exercised(backed by captured action receipts, computed from the cross-check), ordeclared-only(asserted, never wired or used) — and reports the breakdown.treeship resolvesurfaces the same breakdown, so a resolved agent's capability mix travels with it. A new optionalcapability_provenancefield onagent_card.v1(additive; cards without it are treated as fully declared). The first step from a registry of claims to a registry of captured truth. Seecapability-provenancespec. -
treeship attest card --from-harness <path>. Builds a card's capability set from the agent's real harness config (a Claude Codesettings.json'spermissions.allowlist) instead of a hand-typed--toolsstring, and stamps each entrycapturedwith itssource. Deterministic capture at the source: the capability set comes from observed config, not the operator or the model.--toolsentries (if also given) are merged asdeclared.
Older releases
Read the complete release history in the repository.