CHANGELOG.md

This Kit feature is not available yet.

Preview

Text · 383.2 KiB

Changelog

All notable changes to mkit are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

  • Keep connectrpc and buffa caret requirements within one minor train and probe the latest compatible patches in CI. Moving either minor train is a breaking mkit-rpc change; consumers must adopt the corresponding mkit-rpc release. No version requirement changes in this update.
  • Keep atomic force pushes consistent with concurrent pack-chain re-baselines. Head-only writes compare the packmap in the same advance, and appending writes re-plan when their captured chain has been replaced. Dependency plans also compare the observed head, including when a reset restores an identical packmap digest. Force retries share the existing eight-attempt CAS budget; exhaustion returns PushError::PackmapContended. This adds at most one strong packmap read on the uncontended force-push success path. Non-force requests, wire formats and stored formats are unchanged; no store reset is required. Custom atomic transports whose endpoint cannot encode Any can override the new provided Transport::atomic_force_head_condition method; source adapters forward it.
  • Add independent release plumbing for mkit-primitives: its own crate changelog, version and train-exclusion guard, a signed-tag mkit-primitives-vX.Y.Z publish workflow with an idempotent content-compare policy and a semver baseline, and an out-of-tree consumer check, native and wasm32, that builds the packaged crates and consumes the portable fixtures. Nothing is published by this change; no API, wire or stored-format change.
  • Add decide_clearance to mkit-primitives. It recomputes the contiguous published prefix from host-supplied facts and never settles an acceptance reservation or records a second terminal outcome. Add decide_accept_and_clear, which returns the unchanged ticketed-advance decision and, only when that decision applies, a clearance over the same repository and operation, plus ClearanceAdvance::with_pair, which takes the dependency and external-supplier sets from an established resulting-pair coverage. The reference prefix walk uses decide_clearance. No wire or stored-format change; no wipe.
  • Pin one refusal order and classification across inline, scheduled and driver-based pack verification (SPEC-PACKFILE §§6.1-6.2, SPEC-SERVER §9.4). Inside a delta entry the depth check now follows base resolution and precedes decoding. Frame caps are reported in wire order, so an earlier entry's content refusal outranks a later entry's cap (PackDecodeCursor reports a frame's cap when it reaches the frame and gains resume_checked). A bad signature is judged per ticket right after that pack's decode, and denial is a policy check after integrity and after every content refusal. Corrupt stored base bytes are unavailable on every path. Resulting-pair coverage uses one lag-aware miss mapper, so a missing object is retryable inside the membership lag window on both paths. A completed integrity marker is kept for the rest of Decode, so a membership wait never rescans the source. The closure walker treats held and unpublished members as absent and reports a file-integrity fault only after every absent object. The driver checks depth before decoding, reports corrupt stored-base bytes as UnavailableInput, and reports an external depth overflow as ExternalDeltaChainTooDeep (a chain mixing in-pack and external links is reported by its direct base). mkit-primitives adds FailureCode::persistence, classify_membership_miss and FailureCode::ExternalDeltaChainTooDeep. No wire or repository stored-format change; no read-probe change.

Embedder migration entries use [embedder: breaking API], [embedder: stored-format change] and [embedder: store reset required] tags. Combine tags when applicable; every breaking API entry states a replacement and every stored-format entry states whether a store reset is required. Embedders collect the applicable entries between their old and new immutable pins.

Breaking (toward 0.6)

  • [embedder: breaking API] mkit-primitives::ComputeConfig adds a raw-entry dispatch threshold and becomes non-exhaustive. Migration: replace struct literals with ComputeConfig::new(max_jobs). The default threshold is 64 KiB; use .with_min_job_bytes(0) to dispatch every entry. This only changes scheduling; evidence, work charges, refusals, and the checkpoint format stay the same. No store reset is required.
  • [embedder: breaking API] Freeze of the mkit-primitives decision and verifier surface. Every public step, decision, outcome, refusal, state and record enum is now #[non_exhaustive]: Step, ComputeDisposition, ClosureStep, ClosureRequest, ClosureResponse, ClosureCheck, CanonicalSource, PairResponse, FramedEntry, DependencyRecord, Signature, Domain, SignatureResult, ClearanceDecision, ClearanceState, ClearanceMoment, ClearanceLifecycle, ForkAdvancePhase, ForkAdvanceDecision, ForkAdvanceRefusal, ForkActivationDecision, ForkCompletionDecision, ForkLifecycleProtection, ForkObservation, ForkPairContext and ForkVisibility. Migration: add a wildcard arm to every exhaustive match, and make it fail closed (refuse, retry or stop): an unknown step, decision or refusal must never be treated as permission to advance, publish or clear. Every public fact, request, limit and proof struct a host builds is also #[non_exhaustive] with a constructor, and keeps its public fields, so a host assigns fields after construction instead of writing a literal. Migration: replace each literal with T::new(..) (every field, in declaration order) and, for the structs with natural defaults, with the short form and field assignments: Snapshot::new(repository) (nothing acquired), RefWriteRequest::new(repository, kind, changes, required, now_ms), RefRequirements::new(membership_window), ForkFacts::new(repository, request, apply_attempt, acceptance_point) (every observation unacquired), ClearanceFacts::new(repository, moment, prior, changed, previous_state), ClearanceAdvance::new(sequence, generation, operation, state, revision), ForkAdvanceFacts::new(binding, destination, progress, construction_expires_ms), ForkActivationFacts::new(advance, expected_result, inherited_packs, unit) and ForkCompletionFacts start with no saved accepted result, so the decision refuses until the host sets it and ForkCompletionFacts::new(advance, expected_result, cursor, part, packs, digest, barrier_owner). Output structs a host only reads or destructures (for example AcceptanceEffects and its parts) stay exhaustive on purpose, so a new effect field fails the host's build. The set of frozen structs is: the decision facts, Snapshot, the ref-write and ticketed-advance requests and bindings, HostAttestation (new(repository, packs), because an empty pack list makes no pack-identity claim and must be an explicit choice), BasisCfg, and the fork and clearance facts and limits. cargo semver-checks -p mkit-primitives --all-features has no published baseline to compare with (the crate is unpublished), so it was run against the tree before this change (--baseline-rev): five lints fail, all intended and all listed above (enum_marked_non_exhaustive, struct_marked_non_exhaustive, struct_missing for the five removed Guarded* types, struct_with_pub_fields_changed_type for RequiredSourceFence, and type_mismatched_generic_lifetimes for CombinedAcceptance). Wire and stored formats are unchanged; no wipe is required.
  • [embedder: breaking API] Fork acceptance, progress, activation, completion and published-clearance decisions converge on decision::Guarded<D> with owned, typed RequiredGuards. GuardedFork, GuardedForkAdvance, GuardedForkActivation, GuardedForkCompletion and GuardedClearance are removed, with their borrowed required_guards() snapshots, matches, valid_through_ms, evaluated_at_ms and source_fence. Migration: take the decision from Guarded::decision() (a reference; dereference the Copy enums), lower required_guards() (new RequiredGuard::Row, GuardRow and GuardKind::{Fork, Clearance}; handle them in exhaustive consumers, and treat an unknown guard as a failure, never a drop), read the final source proof with fork::fork_source_proof (now RequiredSourceFence::Fork), and compare with decision::guards_hold against the live guards from each facts type's new guards() method. A preparing decide_fork_advance that advances carries the construction expiry as a ValidThrough guard, and ForkClearanceProof has a new required denial_revision that activation guards. RefRequirements::source takes PublishedSourceFence (the former RequiredSourceFence struct), and RequiredSourceFence is now a non-exhaustive enum, and CombinedAcceptance no longer takes a lifetime parameter. ForkPlan gains a commitment(). The reference server's publication prefix now passes the real row revisions to decide_clearance and accounts for every guard it returns; wire and stored formats are unchanged and no wipe is required.
  • [embedder: breaking API] Sealed acceptance basis for ticketed acceptance in mkit-primitives. VerificationVerdict gains a lifetime and its Satisfied variant is replaced by Verified(&[AcceptanceBasis]): pass one basis per accepted pack, built with AcceptanceBasis::from_verified (sealed pack, closure proof, witness pages and the external delta-base facts you supplied during verification, every re-request included, which must reproduce the pack's sealed ExternalBases commitment, VerifiedPack::external_bases()), from_evidence (decoded AcceptanceEvidence) or attested_by_host(HostAttestation) (a host that verified with its own pipeline; it derives guards only from the fields it carries). The decision adds the basis guards to its guard set, so RequiredGuard gains MembershipWitness, DenialVersion, DependencyPublication and Binding and GuardKind gains Binding; DependencyPublication names its object. A ticket whose pack (identity and length) no basis covers is InvalidResponse; HostAttestation::packs names the packs an attestation covers (empty covers any pack, an explicit choice). Both enums are #[non_exhaustive], so audit every _ => arm in guard lowering: a wildcard that drops the new guards is a fail-open. An attestation without fields derives none. Snapshot gains basis; fill it with the values your store currently yields (not a copy of the required guards), or leave it empty. ClosureWalker::new takes &VerifiedPack after the view and refuses a view or ruleset that differs from it. Guarded::into_lowering returns the owned parts. The new synthetic-artifacts feature fabricates a sealed pack for consumer tests only. The all-entry verification checkpoint is now version 4 (it adds the external-base commitment); version 3 checkpoints are not decoded, so restart them from the immutable source. The reference server attests with no fields, so its guard set, reads and stored formats are unchanged; no store reset is required.
  • mkit_sync::RetryReason adds PackmapChanged for a force re-plan whose transfer exceeds one advance. Migration: handle that variant when matching retry reasons exhaustively. mkit_sync::PushError and the CLI's DispatchError add ForceCapabilityUnknown: handle that refusal in exhaustive matches. Connect force pushes now refuse before uploading if discovery failed or was invalid; reconnect and retry. Existing variants and non-force errors are unchanged. No wire or stored-format change; no store reset is required.
  • [embedder: breaking API] Guard completeness for the ALPHA decisions in mkit-primitives. (1) TicketedAdvanceRequest::authority_generation is removed and TicketFact::authority_generation is Option<Revision>: request ticket fencing with write.required.authority, supply the authority observation in Snapshot::authority, and convert a numeric generation with the same Revision encoding on both sides; the decision now emits the Authority guard. (2) ReplayFact gains scope, AuthorityFact and GrantFact gain namespace; a wrongly scoped observation is InvalidResponse and guards_match compares the key. (3) RetryReason gains AdmittedRefChanged (was ChangedFacts) and MembershipNotVisible (was Unavailable); map the first to a ref conflict and the second to a retryable unavailable. (4) ForkRefusal::LifecycleUnavailable is removed; activation and completion return Withhold(ForkWithheld::LifecycleUnavailable) (a new variant of ForkActivationDecision and ForkCompletionDecision: add the arm to exhaustive matches), which never refuses the saved result, and fork acceptance reports an authority fence that cannot be compared atomically as retryable UnavailableInput (was UnsupportedCapability). Wire and stored formats are unchanged; no store reset is required.
  • [embedder: breaking API] Decision behavior now matches its guards. A ticketed advance guards every ticket row it examined (also on a stored conflict), the exact storage counter whenever it counts inline, and lists each consumed pack once in AcceptanceEffects::membership. decide_ticket_cleanup returns InvalidResponse unless each aborted reservation is still Ticketed for its claim, and guards every ticket row up to the first failure. decide_reservation_settlement of a Ticketed reservation also closes the bound ticket (or guards its observed absence): supply the ticket observation in the snapshot. A stored refusal is decided after the grant, authority and lifecycle bindings. A decision whose guards contradict each other returns InvalidResponse. A published-source fence answers an unpublished or unpaired source with not_found "source not found", and a destination pair that differs from the captured pair with a retry. required_guard_kinds no longer demands a Membership guard for a sealed decision's own pack-membership additions. Migration: lower the new Ticket, Storage and Authority guards and the membership addition, and supply the extra observations. No wire or stored-format change.
  • [embedder: breaking API] The in-pack decoded-byte budget is no longer persisted as a rejection by scheduled verification; it is a terminal DecodeBudget outcome, as inline already answered. Migration: none for callers; a stored Rejected row with that message is no longer written. ClosureWalker reports a file-integrity fault (wrong-kind chunk, bad chunk bytes, manifest total) when the closure completes rather than at the fault, and treats held or unpublished members as absent. Migration: expect the fault from the final step, after any Missing verdict would have been reported. PackDecodeCursor::new no longer returns an entry-geometry cap; resume reports it when it reaches that frame (migration: handle the cap from resume). FailureCode gains ExternalDeltaChainTooDeep; the deferred refusal checkpoint table appends tag 20, so discard unsupported verification checkpoints and restart. No repository store reset is required.
  • [embedder: breaking API] All-entry verification pages are now scoped to the run, cached, and counted live. The driver keeps a write-back page cache of resident_pages * page_bytes bytes charged at actual page sizes, flushes a sequence only when its buffered records fill a leaf, at a checkpoint, or at the seal, and hands the host a page only on eviction, at a checkpoint, and at completion of a durable run. A new Step::ReleasePage(PageCommitment) tells the host a page it holds is unreachable from the roots and from the latest checkpoint (no reply). VerifyLimits::evidence_pages and evidence_bytes and ResourceUsage::evidence_* now count pages the host holds, not every page produced; the work ceiling stays cumulative. A 24-entry pack that cost 106 emitted pages and 154 page fetches now costs 14 and none. Migration: handle Step::ReleasePage by dropping the page (never ask for it again); after checkpoint() refuses with CheckpointNotReady, keep stepping (serving retentions and fetches) until the driver returns Progress at a drained entry boundary, then call checkpoint() once more before the next mutating call, and persist the result before stepping on. Hosts that never resume use VerifyDriver::new_with_retention(.., PageRetention::Ephemeral), which refuses checkpoints with UnsupportedCapability and keeps final pages out of the host. Cached pages count against resident_bytes at their actual sizes, up to page_bytes * resident_pages (4 MiB by default, against the earlier single 1 MiB retention reservation); the cache sheds its own clean pages when its own growth would not fit, but other reservations do not, so a caller with a tight resident_bytes should lower resident_pages. Object, occurrence, ordinal, and head roots are unchanged. The dependency root differs from earlier runs wherever one entry's dependency records cross a leaf boundary (an entry with two or more records can): sequences now grow one leaf at a time, so every root equals the tree built by appending the records singly and no longer depends on how flushes or checkpoints batch them. Verify checkpoints stay version 3 and retention_count now counts cumulative acknowledgements, at least the pages still held. Pack and object bytes are unchanged; no repository store reset is required.
  • [embedder: breaking API] The all-entry verifier reports a cumulative decoded total above IndexedProfile::decoded_bytes as PackfileTooLarge and above the caller's own VerifyLimits::decoded_bytes as ResourceLimit; both were PackfileTooLarge. Migration: treat ResourceLimit as a retryable execution refusal. In-driver index lookups and the resume walk now also refuse a child at the wrong height or a leaf key outside its inherited interval with InvalidEvidence. No wire or stored-format change; no store reset.
  • [embedder: breaking API] AcceptanceEvidence::from_trusted_host is removed; it was byte-for-byte decode, and its name implied a trust step that does not exist. Migration: call AcceptanceEvidence::decode with the same arguments. No format change; no store reset.
  • [embedder: stored-format change] All-entry verification checkpoints use envelope version 3 with explicit hash frontiers, an authenticated remaining suffix, and bounded deferred refusal state. Migration: discard unsupported verification checkpoints and restart from the immutable source. Framing checkpoints retain version 1. No pack or object bytes change, and no repository store reset is required.
  • [embedder: breaking API] The ALPHA decision facts now model the exact ticket and pending-reservation observations. Migration: replace TicketFact.authority with authority_generation: Option<u64>, replace upload with upload_session: Option<Vec<u8>>, and supply the reservation-derived ticket identity. Replace ReservationFact::Pending.operation/expires_ms with created_ms, reconcile_ms, and op: ReservationOp; retain operation identity in the request. Add storage: None to Snapshot and an empty storage vector to AcceptanceEffects when counting is disabled. Handle GuardKind::Storage and ReservationTerminalKind::StorageChanged in exhaustive consumers. Wire and stored formats are unchanged; no store reset is required.
  • [embedder: breaking API] [embedder: stored-format change] Scheduled verification job headers add optional VerifyJobV1::integrity progress. Migration: use VerifyJobV1::new, or initialize integrity: None in job literals. This disposable state cannot establish verification. Successful jobs omit the field; no repository store reset is required.
  • [embedder: breaking API] The all-entry pack verifier adds PackCounts::delta_entries and replaces VerifiedPack::external_dependencies() with dependencies(). Migration: initialize the new count in struct literals and read typed DependencyRecord values from the combined dependency sequence to distinguish graph edges from external bases. Verification checkpoints use envelope version 3; framing checkpoints retain version 1. Restart unsupported disposable checkpoints from their immutable source. Pack and object bytes are unchanged; no repository store reset is required.
  • Native pack-zstd now enforces the same 8 MiB effective window and RFC 8878 block maximum as pack-ruzstd. Previously accepted oversized windows or blocks return ZstdDecompress. Migration: re-encode such input with a window no larger than 8 MiB and conforming block sizes. Output claims, cumulative budgets, and stable error variants are unchanged; no store reset is required.
  • The conformance profile adds combined-publication, gc, and async-inspection, with feature-gated publication and removal-race cases. Test-control messages add typed publication and reliance observations. Migration: exhaustive Feature and control-message matches must handle the new variants; declare only implemented optional features. New optional result fields are omitted when absent. No production wire or stored-format change; no wipe.
  • [embedder: stored-format change] [embedder: store reset required] Fork activation uses durable membership permits and drain checkpoints. Migration: reset stores before adopting these formats; no legacy decoder is provided. Authority and epoch setters may return retryable unavailable while a drain finishes. Ordinary writes retain their existing storage round trips.
  • [embedder: breaking API] Low-level fork admission requires source RepoIndex partitions, matching the existing sharded ForkRepo deployment requirement. Migration: use the pipeline's sharded map for new forks; completed receipts remain recoverable. No additional store reset.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Fork preparation now stages membership until guarded acceptance, which atomically saves replay and settles its reservation. Activation uses revocable local permits; generation/epoch moves invalidate them before acknowledgement. Coherent source proofs retain mutation-version evidence across slices. Migration: reset existing stores; no legacy decode is provided. Configure low-level ForkEnv and ForkTimer with with_publication_policy when using a custom pipeline policy. Implement PublicationPolicy::fork_version with a durable, non-repeating version that changes atomically with fork eligibility; the default refuses new and unaccepted forks.
  • [embedder: breaking API] Storage Write::Increment(Key) and Batch::increment atomically advance checked big-endian u64 mutation stamps without another writer round trip. Migration: implement this variant inside each backend's existing atomic apply; absence starts at zero, and malformed values or overflow reject the whole batch. Update exhaustive write matches. Existing stores must be reset with the fork-format change above.
  • Conformance profiles declare transport writes, authority RPCs, authority fencing, fork, and the relay-lag bound. Results distinguish NotApplicable from declared Skip. Migration: declare offered features and handle the new verdict; remove skip entries for absent features. No stored-format change or wipe.
  • The conformance crate's storage suite is now optional under storage-spi; the default wire and server-control library does not depend on mkit-server. Migration: enable storage-spi when invoking the storage macros. Profile::server_clock accepts the independent wire::TestClock trait; reference ManualClock remains supported when storage-spi is enabled. No protocol bytes or stored state change; no wipe.
  • [embedder: stored-format change] The server encodes in-progress resumable publication verification as a versioned pair-coverage checkpoint instead of per-field walk state. In-flight verification rows carry the old encoding and cannot be resumed after upgrade. Store reset: yes, only verification rows carrying in-progress publication progress. Public APIs, wire formats, and all other stored formats are unchanged.
  • mkit-git-bridge::import::ObjectSink requires reuse_object: refresh the cached object's retention time and return its immediate history references, refusing missing objects. Implement this method in custom sinks; reuse a DepthMemo across per-ref importers in one run. In-memory sinks may simply return decoded references. No stored-format change; no store reset.

Added

  • Verify pack integrity and canonical entries in one acquisition pass. Entry-boundary checkpoints resume without reacquiring the consumed prefix; integrity failures retain precedence over deferred entry refusals.
  • Add portable ticketed-advance and reservation-settlement decisions to mkit-primitives: request-ordered ticket validation, upload-proof claims, pending-reservation stability, replay precedence, authority-generation fencing, and the composed ticket-closure, settlement, storage-counting and durable-outcome effects. The defensive PackMissing cleanup and standalone reservation settlement share the same portable rules. The reference server decides ticketed advances through them and verifies every sealed guard projects to a physical precondition. No wire or stored-format change or store reset; source migrations are listed under Breaking.
  • Add optional caller-owned compute scheduling to mkit-primitives pack verification. Immutable jobs and opaque outputs bind the run, frame, inputs, operation, and shared memory/work permits. Raw Blob jobs can overlap; ordinal merge preserves serial evidence and refusals. Other objects and deltas remain dependency barriers, and serial execution stays the default. Additive ALPHA API; no protocol bytes, checkpoint format, or store reset.
  • Add bounded all-entry VerifyDriver verification to mkit-primitives, with canonical object identity, earlier-entry and repository-scoped external delta bases, individual signatures, authenticated paged evidence, and cold resume. Object fragments stay provisional until Step::Complete seals a VerifiedPack. Resource limits cover decoder scratch, evidence, and work.
  • Add portable guarded ref and publication decisions to mkit-primitives, with exact keyed dependencies, refusal-only guards, finite work bounds, semantic fixtures, and seeded transition replay. The server checks physical ref and publication projections against sealed decisions. Additive ALPHA API; no wire or stored-format change, migration, or store reset.
  • Resulting-pair coverage in mkit-primitives: closure::pair::PairWalker is a sans-IO pull machine over packmap-chain acquisition, reachable-object traversal, membership facts, external delta-base suppliers, and the fork cleared set. pair_requirement decides the coverage obligation, PairLimits supplies every bound, PairWalker::checkpoint/resume carry local resumable progress under a corruption-check format, and PairCoverage is a sealed artifact. Denial, inspection, publication state, and authorization stay separate host guards. Adds FailureCode::PackmapCycle. Additive ALPHA API; no public API break; no wire change.
  • WindowCursor::metadata returns validated, read-only pack identity and entry geometry for binding caller-owned checkpoints. Cursor fields remain private, and cursor encodings and reader verification behavior are unchanged.
  • Add pure fork progress, membership activation, and clearance completion decisions to mkit-primitives and adopt them in the reference fork driver. Recorded authority and exact pending reservation guards retain their commit boundaries. Accepted clearance preserves its saved result and never repeats settlement. Additive ALPHA API; no wire or stored-format change; no reset.
  • Add bounded in-process acceptance-evidence export with canonical signed head checks, exact closure witness-chain authentication, and cross-artifact binding. Evidence format 1 is unchanged; no migration or store reset.
  • Add guarded fork acceptance to mkit-primitives: bounded full-inventory plans, authoritative destination emptiness, final source-proof ordering, exact authority and admission guards, terminal reservation settlement, and saved result replay. Acceptance creates no visible membership or destination refs. Additive ALPHA API; no wire or stored-format change; no store reset.
  • Portable auth-v2 validation, upload-claims binding, and replay classification in mkit-primitives, with raw header multiplicity checks, shared vectors, and deterministic claims replay. Hosts retain token formats, key custody, authorization, storage, and atomic application. APIs are additive; no wire or stored-format change or store reset.
  • A versioned acceptance-evidence envelope for trusted verifier and writer hosts, with strict bounded decoding, frozen operation binding, canonical digest, and independent head signature rechecking. This adds a library byte contract; existing server paths and stored formats are unchanged. No store reset.
  • Add bounded sans-IO pack framing, authenticated evidence pages and paths, and a strict versioned checkpoint envelope to mkit-primitives. Framing completion remains separate from canonical object and signature verification. New reconstructible evidence/checkpoint formats start at version 1; unsupported state requires restart. Existing pack and object bytes remain unchanged. No existing caller migration or authoritative store reset.
  • Add the independently versioned mkit-primitives ALPHA contract skeleton. It defines opaque verification, closure, and guarded-decision types without changing existing runtime paths or wire formats. No migration or store reset.
  • Published-clearance requirements now state the acceptance boundary, atomic membership and prefix publication, configured Event responsibility, and protection against duplicate settlement. Combined acceptance and clearance are permitted with all clearance guards. Draft embedding composition declares atomic domains without requiring a Rust API. GC reliance fences permit per-member mark clearing or a repository reliance version; membership witnesses enumerate removal invalidation and retain independent policy checks. No production wire, runtime, or stored-format change; no wipe.
  • The standalone conformance runner adds run for the wire and server-control tiers, and diff for deterministic protocol decisions against an embedded native reference host. Versioned profile files declare features and controls; JSON reports and documented exit codes support CI. Enable reference-runner when installing the optimized differential test utility. Production artifacts do not enable test controls. No wire or stored-format change; no wipe.
  • Protocol acceptance and recovery requirements now distinguish portable guarantees from reference-server mechanisms. Fork acceptance requires a final source proof after membership staging, with uniform source-race refusal and ordering at the proof. Tier-2 assertions report implementation coverage separately from declared skips. The conformance library and runner are prepared for the next lockstep crates.io release, with a versioned test-only control binding and declared server-control coverage. No production wire, runtime, or stored-format change; no wipe.
  • Sans-IO direct-child closure with bounded frontier membership batches, paged visited state, complete missing-set evidence, and exact supplier witnesses. Tree/tag child closure preserves existence-only semantics; chunk integrity covers every consumed manifest. Hosts retain and acknowledge private evidence pages, and replay verification after evidence loss. New local evidence format; no transport change or repository store reset.
  • ReadAccess::declared_visibility reads the stored visibility declaration whether or not the repository record exists, for private-origin locks and similar flows that declare visibility ahead of the record. It returns None when no declaration exists and never substitutes the configured default; its opaque revision brackets the declaration like repository_visibility's. StoreError::MalformedReply marks backend replies that violate the storage contract, such as short or oversized batch results, apart from corrupt stored rows. Additive APIs; no wire or stored-format change; no wipe.
  • Typed embedding reads now include exact object-index lookups within supplied packs, with validated frame bounds and ordered results, plus fresh membership and published-member witnesses with the internal clearance rule. ForkJobStatus::packs already includes confirmed pack lengths in bytes. The opt-in mkit-server/test-support feature supplies typed test seeders; embedders' tests should use it instead of adapter_spi codecs. APIs are additive. No wire or stored-format change; no wipe.
  • Opt-in perf-metrics builds emit one fork_step event per fork slice and one fork_step_summary per job reaching a terminal phase, attributing step executions, storage calls and serial rounds per partition kind, and precondition losses to the request or alarm driver. The conformance probe fork.driver_probe forks a fresh source over the __test-faults Worker route at configurable file counts (MKIT_FORK_PROBE_FILES, default 500,3000). Default builds are unchanged. No wire or stored-format change; no wipe.

Fixed

  • The native HTTP channel (mkit-sync connect-native, and so mkit-client over it) no longer fails a read when the server closes a pooled keep-alive connection just as the request is written to it. A read RPC (or GET/HEAD) that fails with a closed-before-response error on a reused connection is resent once on a fresh one. Signed writes, unknown procedures, streaming uploads and failures on a fresh connection are never resent by the channel; a write still surfaces the transient connection error so the caller can retry with its signed identity.
  • Scheduled pack verification now applies SPEC-PACKFILE §§6.1 and 6.2 when integrity and entry faults compete. A corrupt trailer with a missing delta base or an indexed decode-limit violation produces the same persisted invalid_argument: object hash mismatch rejection as inline verification. Previously, scheduled verification could select the base or limit refusal first. A missing base with a valid trailer retains its membership-lag handling and never becomes a persisted rejection. Extra source reads occur only after an early entry refusal; successful verification adds no reads.
  • Preserve reference diagnostics for partial signed envelopes in the portable auth header precheck, including a missing version, audience, content commitment, validity, signature, idempotency key, or unary body digest. A missing X-Repository stays with routing, which classifies it per deployment (invalid_argument on multi, unauthenticated on single). Authentication codes, duplicate rejection, and verification order remain unchanged.
  • Unfenced multipart and stream writes no longer read the fork permit marker during authority activation. That marker is consulted only when fencing is enabled, inside the existing activation read. A full coordinator partition during an epoch or authority move is again unavailable with storage partition full, not fork unavailability. No API, wire, or stored-format change; no store reset.
  • Use smaller test codegen units and serialize Cloud Build's workspace test compilation on the existing worker to bound compiler memory. All tests and their execution concurrency remain unchanged.
  • Update rustls to 0.23.46 to address RUSTSEC-2026-0346 through RUSTSEC-2026-0349 without new advisory exceptions.
  • Restore the web terminal's Sandbox SDK pin to 0.12.10, matching the backend terminal protocol and restoring the SDK's xterm export.
  • Auth-v2 servers reject incomplete signed metadata on reads whenever any signature-triggering envelope header is supplied. Repository addressing and an idempotency key alone remain unsigned. Duplicate envelope fields fail closed before routing. Signed reads still bypass replay, and replay never extends envelope validity. The compressed-read check uses the expanded signature-trigger set in every authentication mode. Existing unsigned client writes retain their authentication-mode behavior. No wire schema or stored-format change; no store reset.
  • Fork publication absorbs a competing driver's exact witness pair after a guard loss. Foreign rows still refuse the fork. Retryable job-step errors now carry Retry-After: 1, retaining unavailable and HTTP 503. Error message strings change: fork publish contended becomes fork publish raced for unresolved races or fork publish deadline for expired apply windows; fork copy contended becomes fork copy deadline. No API, wire schema, or stored-format change; no drain or store reset is required.
  • ReadAccess::recover_completed_fork now recovers a Done fork whose reservation was deleted by outcome acknowledgement or whose replay row was pruned after envelope expiry. The Done job is committed atomically with both rows and stays authoritative: its result and settle identity are still matched exactly against the caller's identity, the authority and namespace checks are unchanged, and a row that is still present must agree with the job or recovery fails closed. No wire or stored-format change; no wipe.
  • Scheduled indexed extraction boxes each stage future so nested backend reads leave room for frame decoding on a small stack. The 50-hop quota-boundary regression retains its 512 KiB stack, and a native future-size guard covers the extraction steps shared with wasm32. Checks, source charging, stored bytes, public APIs, and wire formats are unchanged; no store reset is required.
  • Fix bulk-writer reuse of read-only object files on Unix. Local gc now excludes its final age check and unlink from concurrent bulk refreshes; a crashed sweep leaves objects at their canonical paths. Git import refreshes complete cached closures once per run without re-hashing. The strict grace-window bound still applies to concurrent imports. No object format or layout change; no store reset.

Changed

  • pack-ruzstd selects the MIT-licensed mkit-ruzstd package through a versioned path dependency. Published consumers inherit the bounded decoder. Embedders can remove their workspace ruzstd patch after updating to this release. The release pipeline publishes the decoder before the workspace. Wire bytes, object budgets, and stored formats are unchanged; no reset.
  • Windowed pack checkpoints reuse an incremental BLAKE3 prefix digest. Cold resumes authenticate and rebuild the current prefix once. Cursor bytes, public signatures, refusal order, and final pack integrity checks are unchanged.
  • Commonware dependencies accept compatible 2026.x releases. Committed lockfiles retain reproducible builds; a weekly and manifest/lockfile PR compatibility check builds and tests the latest release against pinned wire bytes. No API, wire, or stored-format change; no store reset is required.
  • Performance: ClosureResolve and EmitIndex outside the tiny-pack path share up to eight durable slices per callback, with the original timer and job guards and shared call and time limits. The additive SliceLimits::with_closure_chain_slices(1) and with_emit_chain_slices(1) settings restore single-slice callbacks for each phase. Verification probes now cover takedown denial on and off and attribute Decode storage work by sub-step. No wire or stored-format change; no wipe.
  • Additive ForkEnv::from_config and ForkTimer::from_config constructors share fork proof settings with PipelineConfig; the pipeline and configured Worker timer use them. Existing fields and low-level registration remain available. The operations runbook now covers backfill, draining forks, and the permanent 4,096-item publication limit when enabling takedown denial after running with it off. No behavior, wire or stored-format change; no wipe.
  • Performance: Extract local selection outside the tiny-pack path can share up to eight durably committed slices per alarm callback, retaining the existing call, time, step and batch limits and fresh checks at every checkpoint. Use the additive SliceLimits::with_extract_chain_slices(1) setting for single-slice callbacks. No wire or stored-format change; no wipe.
  • [embedder: breaking API] Typed embedding reads now classify errors. Corrupt or contradictory stored rows fail as permanent internal; malformed replies and backend faults stay retryable unavailable, and invalid_argument is unchanged. Callers that retried every unavailable now see internal only when stored data itself is corrupt: treat internal as permanent and keep retrying unavailable only. Wire RPC error mapping and absence semantics are unchanged.
  • Small pushes, including the commit right after a fork, no longer repeat identical backend reads inside one request. Each upload session now latches its unfenced authority-activation probe instead of re-reading the fence pair at every stage, finish and completion check (fenced namespaces still read the authority generation live on every check, and every apply keeps its generation and lease-recovery guards). The ticketed advance memoizes each ticket's marker/pack head proof per request, shared across the planner's replans and the pending-hold retry. The scheduled-verification usability check fetches the member rows its failed-group and finished-peer guards need in one batched read, batches the first pages of dependency and owed-children scans, and fuses per-commit history and head-type frame lookups across consumed packs. The write-round probe's embedder upload step drops from 13 to 7 KV calls without denial (23 to 11 with), and the held commit retry issues no repeated blob heads. Admission, replay, verification, denial and abort semantics are unchanged. No API, wire or stored-format change; no wipe. See the request write rounds probe.
  • A fork copies the source's index partitions in waves of up to six instead of one at a time, and reads larger pages (500 rows, or 256 where holders are copied; they were 64 with extraction). The Copy phase of a 500-file fork takes about 9 serial rounds instead of 35, a 3,000-file fork about 15 instead of 112, and a 10,000-file fork now finishes in one slice (35 rounds) instead of three (340). A page with more holder candidates than one wave may copy resumes inside the page. Index rows, holders, checks, failure order and budgets are unchanged: a wave reports the first failing partition in order, and one that spends the slice keeps no progress. [embedder: stored-format change] [embedder: breaking API] The fork job row gains one copy cursor per partition (lanes, of the new CopyLane), and part and after no longer carry the copy cursor. A job started by an earlier build cannot be decoded: finish it before upgrading or discard it; no store reset otherwise. Code that builds a ForkJobV1 literal adds lanes: Vec::new(); reading jobs through decode_job needs no change. No wire change.
  • A fork takes fewer serial storage rounds. Without pack-level takedown denial it skips the closure walk and writes no cleared set (nothing read it; enabling denial later makes the first advance of such a destination a full walk, slower but equally strict, and bound by the usual per-advance item capacity). A plan that fits one slice is computed once, before admission, and carried into the job, which refuses it unless the source still publishes the same tip, packmap head and membership generation; the first proof of each pack after admission now precedes its membership write, so a takedown landing in that gap fails the fork after registration like any later refusal. Without denial, and where the store reports a known read allowance, planning reads up to six packs at once and publication writes and proves the members other than the head in waves, then writes the head last. A 500-file fork of a two-commit source takes 101 rounds instead of 142 and a 3,000-file one 178 instead of 219. New additive API: fork::plan_fork, fork::ForkPlan and fork::start_planned (start_with and plan_bytes are unchanged). No wire or stored-format change; no wipe.
  • The reachability walk behind object reads now expands trees first and then alternates history with a small, doubling file batch, instead of locating every file entry of a tree before following any commit parent. Reading an older commit, or anything inside it, no longer costs O(entries) of the wide tip tree per read. The proven reachable set, uniform absence, takedown stops, the object cap and per-object error handling are unchanged; only the order of exploration differs. Which objects load before a target is found, and so what an early NotFound or a decode or call budget stops on, can differ, and a chunk behind a tip manifest in a wide tree now follows a few history loads. No API, wire or stored-format change; no wipe.
  • ReadAccess::recover_completed_fork now recovers a completed fork that created its destination namespace (a first fork into a new namespace) instead of failing closed. Replay, reservation and settlement checks are unchanged; the namespace record must also be present and well formed. Grant-epoch forks still fail closed. No wire or stored-format change; no wipe.
  • Optional repository index activation now covers anonymous readable repositories after existence and authorization checks, joined with the next read step. REPO_INDEX_PREWAKE_ANONYMOUS defaults on under the default-off master switch; operators can select signed-only activation. Anonymous waves retain repository TTL claims and add an isolate-wide cap of eight waves per rolling minute (256 extra Durable Object calls), within the existing invocation budget. Both signed and anonymous plans now ping all sixteen RefIndex buckets before the sixteen RepoIndex shards, including Multi ListRefs after authorization and token validation. Two sequential groups retain concurrency sixteen; both families share one TTL claim and a thirty-two-call reservation. Private, missing and invalid repositories never start anonymous waves. No wire or stored-format change; no wipe.
  • Extract shares one etag-bound pack window across each slice's staged non-delta chunks, capped at 4 MiB plus 240 bytes of frame headers and the configured window limit. Frames outside the buffer use bounded per-frame reads. Each chunk retains its hash, size, and identity verification; protection, denial, lease, and replay boundaries are unchanged. No API, wire, or stored-format change; no wipe. See the Extract call report.
  • mkit+https and mkit+http push, fetch, pull, and clone route through ClientSource and mkit-client. mkit-transport-connect retains a native host adapter and its public transport, epoch, and visibility APIs; its independent per-RPC driver is removed. Native uploads and downloads stream through bounded frames. Metadata/transfer budgets and recovery limits remain; attempts are clipped to remaining advance time. The adapter guide records HTTP, coding, runtime, connection, and stricter failure-validation differences. Reads and signed writes reuse one idle connection per origin for ten seconds; automatic stale-connection retries are limited to untouched requests before writing starts. The current-thread scheduler and dependency bounds remain. Pack and packmap bytes and ids, receipts, CLI output, and exit codes stay unchanged. No server, wire, or stored-format change; wipe: no.
  • A signed mkit-sync::ClientSource supports native push through mkit-client per-RPC writes. Caller-owned request policy supplies grants and admission credentials; WriteRecovery owns helper execution and scheduling. Shared ClientSession retains upload commitments across borrowed clients and refuses reuse at another destination or signer. Channels can distinguish transient I/O from terminal refusals through additive send_classified/ChannelError APIs. Empty uploads include the explicit final chunk used by the existing transport. Shared PartReceiptStore types preserve durable part acknowledgements and resume across fresh client or transport instances. Unsigned clients with an explicit clock retain write nonce and expiry metadata without a signer. Client additions are additive. No wire or stored-format change; wipe: no.
  • A pending PendingVerification answer now carries the scheduled job's actual timer wake in milliseconds for every phase of a tiny job (the existing SP2 VerifyJobV1::tiny bound), with the 100 ms floor and the existing seconds cap retained — previously sub-second hints applied only in AwaitDelivery. Larger jobs keep whole-second rounding. The wire field already carries milliseconds; no wire or stored-format change; no wipe.
  • Fork emptiness scans, source index statistics and holder copies run in bounded waves. Per-pack write denial proofs use four concurrent directory reads when takedown denial is enabled. Successful work uses the same storage calls and slice allowances; failures retain input-order precedence, and unfinished units resume through the existing checkpoints. No public API, wire, or stored-format change; no wipe. See fork wave bounds and latency model.
  • Add opt-in perf-metrics storage RPC summaries, denial duration histograms, verification stage and chain timing, and AwaitDelivery hint measurements. SQL query accounting separates stepping/materialization from wire encoding. Existing verification slice events also attribute Extract scheduling delay, retries, cold activation, the slowest storage call, and slice stop reasons. Default builds retain their existing cost and behavior. No public protocol or stored-format change; no store reset is required.
  • Ingest and read paths skip block and denial-action reads when PipelineConfig.takedown_denial is false. Holder, inventory, membership, index, and proof-structure writes continue. Takedown-enabled checks retain their storage calls, freshness, and failure precedence. Enabling takedown on an existing store requires holder, inventory, and proof backfills; previously ingested objects are checked on read after activation. No wire or stored-format change; wipe: no.
  • Native repository push now lives in mkit-sync: push returns a typed PushReport or PushError, uses RemoteSource, and reports progress through SyncObserver. The CLI delegates closure and delta planning, pack upload, split advances, and packmap CAS retries while retaining configuration, trust, grants, admission policy, output, and exit codes. Pack and packmap ids, ref moves, limits, and resumability are unchanged. No wire or stored-format change; no wipe.
  • Refill ListRefs scans as each bucket finishes instead of waiting for fixed groups of six. At most six scans remain in flight; each dispatch reserves its inherited call ledgers, and failures drain dispatched reads before returning the earliest source error. Complete ref coverage, pagination, response bounds, and wire/stored formats are unchanged; no store reset is required.
  • Add a default-off, bounded ticketed AdvanceRefs verification hold. Workers configure ADVANCE_REFS_HOLD_MS from 0 through 2,000; core embedders inject a timer and absolute request deadline with with_pending_advance_hold. Readiness repeats all normal guards. Timeout preserves the pending bytes and typed hint. No breaking API, wire, or stored-format change; no store reset.
  • Share pure packmap advance planning between the protocol client and CLI in mkit-core::transfer::packmap. Node bytes, CAS conditions, retries, and driver-specific limits and errors are unchanged. No wire or stored-format change; no wipe.
  • Native fetch and pull stop packmap walks at a fully verified per-branch checkpoint bound to the remote repository and origin. An unchanged branch needs only ListRefs and ReadRef; an appended node downloads only its new packs. Rewritten chains retain the full walk, and missing local objects retain full-chain recovery. New .mkit/applied-packs/.heads/ files are disposable local caches; no wire or authoritative stored-format change, public API break, or store reset is required.
  • Workers deployment guidance recommends explicit region placement, matching Durable Object and R2 location hints, and caller-versus-object trace checks. No runtime, API, wire, or stored-format change; no store reset is required.
  • Re-vendor ruzstd 0.9.1 for RUSTSEC-2026-0344, retaining the stricter RFC 8878 bounds on compressed bodies, regenerated literals and sequence output before allocation. Independent workspaces resolve the patched 0.9.1 source. Embedders using the pure-Rust decoder must update their immutable [patch.crates-io] revision to include this vendor refresh. No API, wire, stored-format or store-reset change.
  • Client replay documentation now distinguishes reconstructing the same authenticated operation, body and identity from exact-byte replay of a retained request: unsigned observer headers may be regenerated. The sync feature comment qualifies its zstd-free dependency claim with default features disabled.
  • Scheduled verification can run up to eight consecutive slices for decoded packs with one to three entries and at most 16 KiB of encoded and decoded data, and for canonical packlists of at most 256 bytes. Every slice commits before the next and shares the handler's existing call allowance. Tiny delivery waits can discover a small outbox immediately through the registered relay handler, retaining its hooks and alarm limits. SliceLimits::with_tiny_chain_slices(1) selects single-slice scheduling. Tiny AwaitDelivery jobs can return a millisecond polling detail with a 100 ms floor and a 3,000 ms cap; HTTP Retry-After still rounds up to seconds, and clients that clamp to one second behave as before. No wire or stored-format change and no store reset. Upload-time job creation remains a design follow-up.
  • Ref listings in mkit-client, mkit-sync, and mkit-transport-connect share ordering, duplicate, cursor-cycle, and resource checks. Cursors are capped at 4 KiB; retained state is charged to a 128 MiB budget, including cursor digests, names, and collection overhead. Complete listings stop at 100,000 pages or a caller's lower cap. Invalid listings return each driver's invalid-response error (mkit-client::Error::Invalid, including page-cap exhaustion that previously returned Error::Limit). Entry-point signatures and wire formats stay unchanged; no store reset is required.
  • Denial waves now contain at most sixteen per-object reads, matching the inventory staging batch. Other read paths retain their shared parallelism of six. Successful batches still charge one call per object and select failures in input order; a failing wave may issue up to fifteen later reads. Staging still precedes admission, and admitted-reader reservations remain all-or-nothing. No API, wire, hook, stored-format, or store-reset change.
  • Scheduled Decode, EmitIndex, and inline index writes share per-object takedown bounded denial waves, after inventory staging and before provisional rows or the direct index apply. The first failing object in batch order still selects blocked or unavailable. Successful batches keep one call per object; a failed wave completes already-issued reads and skips later waves. The inline verifier's pre-staging check uses the same waves at its existing boundary; per-chunk and per-frame extraction checks stay serial. verification_inventory_progress.denial_ms now reports wave wall time rather than summed per-call time. No API, wire, hook, stored-format, project-limit, or store-reset change is required.

Added

  • Typed, uncached reads::ReadAccess queries for repository visibility, published branch pairs, member locations and denial, validated inventory, bounded canonical reads, fork status, and retained completion recovery. Pipeline::read_access borrows the configured routing and visibility default. Replace direct codec, key, index resolver, publication, and fork row reads with these accessors; see typed read migration. Callers retain repository authorization and final disclosure checks. Additive API; no wire or stored-format change and no store reset.
  • Opt-in just cargo-cached, just build-dir-env, and just worktree-gc recipes share sccache compilation results, isolate intermediates per worktree, and reclaim merged/gone worktrees' build output without removing source. A local configuration guard keeps CI and Docker cache paths unchanged.
  • NativeHttp exposes independent with_metadata_timeout and with_transfer_timeout budgets and send_typed for native errors, including NativeHttpError::UnsupportedAcceptEncoding. with_timeout sets both budgets.
  • ConnectTransport::list_refs_with_page_limit accepts a lower caller page cap, clamped to the fixed 100,000-page ceiling. Existing listing entry points keep their fixed cursor and retained-state caps.
  • Typed mkit.transport.v1.MembershipNotVisible Connect error detail (an empty message) on the repository-membership-lag unavailable, emitted at every site that answers repository membership not yet visible (delta-base, closure and packlist checks, and the ref-policy fast-forward walk). Code, message and HTTP status are unchanged, and no Retry-After is added. mkit-client and mkit-transport-connect classify by the detail first and keep the exact message match as a documented fallback for older servers. mkit-server gains ServerError::membership_not_visible() and MEMBERSHIP_NOT_VISIBLE_TYPE. The proto change is additive (buf breaking clean). No stored-format change and no store reset.
  • mkit-client: host policy seams (additive API). Signer::sign_operation (defaults to sign) receives a SigningRequest (procedure, audience, repository, parsed ContentCommitment, attempt) and may refuse with SignError::Refused; Observer::before_dispatch, installed with Client::with_observer or Push::run_with, runs before every write RPC with a WriteIntent (exact ref moves, pack, tickets, new object ids when known), may veto, and may add unsigned per-attempt headers checked against the SPEC-TRANSPORT-CONNECT section 5.1 reserved list. Order: observer, signer, deadline check, dispatch; once per signed envelope (a poll that reaches an envelope's expiry now renews it, as the expiry check does). Client::sign_envelope builds signed auth v2 headers for a host-dispatched procedure through the same pipeline. New Error variants: Vetoed, SignerRefused, ReservedHeader, EnvelopeExpired. An envelope whose window has ended is never sent. Client::advance_refs returns Error::Invalid before sending when the request is malformed (an id that is not 32 bytes, an unspecified expectation, a missing new id), with or without an observer, so the ref moves reported to the observer are exactly the ones sent; well-formed requests are unaffected.
  • mkit-sync gains a mkit-client backend (client feature): ClientSource reads mkit+https and mkit+http remotes through mkit_client::Client, anonymous or signed by an injected Signer and Clock, driven by an Executor. The connect-native feature adds NativeHttp, NativeClock and default_tls_config: a small native HTTP channel (hyper over rustls on a current-thread runtime, gzip as the only content coding, no zstd-sys) for embedders that bring no HTTP client of their own. A CI gate (scripts/check-sync-native-deps.sh) holds its dependency graph. The CLI is unchanged. mkit-client gains Client::set_repo_visibility, Visibility, and Destination::{origin, repository}.
  • mkit-client: the runtime-independent async protocol client for reads and writes, on native and wasm32-unknown-unknown with no tokio, hyper or C compression in its dependency graph. The earlier mkit-push (never published) is renamed to it, with its code unchanged in behaviour (Push, Plan, HttpTransport, Signer, Clock). New: Client with a typed call per RPC (server_info, list_refs / list_all_refs, read_ref, pack_exists, download_pack with a caller size cap, update_ref / delete_ref, begin_upload, upload_pack, upload_part, complete_upload, advance_refs), anonymous or signed reads, and resolve_pack_chain. Per-RPC calls never wait: a pending verification, membership lag, Retry-After hint or admission challenge returns as a typed Error (PendingVerification, MembershipNotVisible, RetryAfter, PaymentRequired with every challenge, also from a bare 402's headers). Every signed call takes an optional caller-owned Identity (nonce, which is the idempotency key, and validity window), uses it exactly, and returns the one it used, so a persisted identity replays the same request after a restart. unpack decodes a pack into an ObjectSink (no filesystem) and verifies commit, remix and tag signatures by default; Verification::Skip is the explicit opt-out. Push::run keeps its waits; they now live only in its own polling loop. A golden test pins pack and packmap node ids for a fixed plan. The pack-ruzstd feature (default) reads compressed packs without a C toolchain; pack-zstd adds the native writer.
  • mkit-sync: a native clone, fetch and pull library for .mkit repositories. It owns the filesystem layer that used to live inside mkit-cli (packmap chain walk and download, the applied-packs record, the per-branch repository lock, signature verification of newly fetched objects, closure check, remote-tracking refs, fast-forward with rollback, clone setup) and returns typed reports or errors, prints nothing, reads nothing ambient, and verifies signatures by default with an explicit opt-out. It reads through a RemoteSource trait whose first backend wraps mkit-core's synchronous Transport, so every existing scheme keeps working. mkit clone, fetch and pull are thin wrappers over it with unchanged output and exit codes. No wire or stored-format change.
  • Embedded dispatch for procedures with no Connect route, fork first (SPEC-SERVER §9.9). Pipeline::dispatch_fork (generic form dispatch_embedded::<P, _> over the new EmbeddedProcedure trait) takes the request headers and the canonical body and authenticates as Procedure::Fork (auth v2, body commitment), runs an EmbeddedGate (the embedder's deployment check, then an optional pre-dispatch policy) and fork_repo, returning the typed ForkResult or the pipeline's ServerError with its status, Retry-After and details. The policy is skipped only for a replay of a completed fork with consistent job evidence; the evidence check moved into mkit (fork::CompletedFork, fail closed) and is also available as the read-only Pipeline::completed_fork, which answers a witness and never a result. The Workers adapter adds embedded::EmbeddedDispatch (and, on wasm32, adapter::embedded_dispatch) which runs the fetch path's deployment guard (sharding_guard::DeploymentGuard, now the one implementation behind fetch, the admin mount and the embedded entry). Embedders no longer need store::adapter_spi::{keys, codec}, fork::decode_job or replay::classify to recognise a completed fork. An embedder can sign the fork request with mkit-client's Client::sign_envelope; no dependency is added. Additive; no wire change, no stored-format change, no store reset.
  • Ticketless same-repository branch creation from an authoritative published pair: AdvanceRefsRequest.source_ref (field 11), optional expected_tip (field 12), and Pipeline::advance_refs_from_published{,_with_meta}. Both destination names are validated before hooks or writes. Both refs use Missing; the server reauthorizes and compares the source publication row before every destination apply. A later source move is concurrent and success copies exactly the captured pair. The immediate path adds no coverage walk, upload or stored bytes; configured publication checks retain their limits. Embedders can replace closure re-uploads or separately read head/packmap pairs with this operation. Operation::published_source exposes the captured ref, pair, generation and published sequence to hooks; remote hook Operation gains source fields 10–14. Existing operation hook bytes are unchanged. These APIs and wire fields are additive; no stored-format change or store reset is required.
  • Ref admission exposes additive AdmissionInput::resolved_ref_updates and pipeline::{ResolvedRefUpdate, RefEffect}. Each ref has its name, new target or deletion, and Create, Replace, Delete, or NoChange (deletion of an absent ref). Original request CAS conditions remain in Operation; correlate the views by ref name, since effects use planner order (packmap before head). Custom ref admission rejects duplicate destination names before invoking the hook, so one ref cannot reserve or settle multiple presence effects. Successful applies fence these effects in their ref batch: conditional writes use CAS; unconditional writes retain the pre-admission presence through retries and abort if it changes. Present-to-present target changes remain allowed. Creating from a published pair reports both destination refs as Create, with their captured targets. Fork creates membership without refs and reports no effects. The remote-hook schema adds AdmitRequest.resolved_ref_updates (field 9). Migration: configured custom Admission now runs on ticketed AdvanceRefs and implicit consuming UpdateRef, with zero declared bytes and no pack. Reserve ref effects separately from upload bytes, retain them by reservation id, and settle once from the outcome with that id; pack outcomes also name refs and must not settle a ref reservation again. The ref reservation commits atomically with the advance and pack outcomes, or aborts on conflict/failure. These paths add one awaited hook call and, when reserved, pending-row and settlement storage work. Custom ref admission precedes mutating inline verification, whose failures settle the reservation within the existing apply deadline. Scheduled readiness polling remains admission-free until verification is ready. Default Admission calls, quota accounting, and unconditional semantics are unchanged. API and hook additions require no stored-format change or store reset.
  • Server-side repository fork engine (mkit_server::fork, SPEC-SERVER §9.9): a crash-safe job that gives an empty destination the published membership of one source branch without a ref. It copies the packmap chain, the packs it lists and the packs that supply their external delta bases, proves each pack with a seal-and-descriptor proof whose cost does not grow with the pack, registers the destination first and publishes membership last, counts each pack once and records a cleared set of the inherited commits and trees. With pack-level takedown denial on, a later publication walk skips a cleared object (never a denial check) when the packmap chain contains the flagged inherited head, so a remix commit on an inherited tree no longer walks inherited history. Measured at 50 ms per call: 500 objects in 274 storage calls, 3,000 in 296 and 10,000 in 400, one slice each. ForkTimer is the kind-16 handler; fork::start and fork::step drive a job.
  • Pipeline::fork_repo (SPEC-SERVER §9.9): the embedder-facing fork. A signed ForkRequest (source, branch, required expected_tip, destination visibility; its canonical body is the signed body: commitment, procedure /mkit.server.v1/ForkRepo) is authorized by the caller's read of the source (every refusal is the uniform not_found source not found) and then by the destination write, with the source's visibility and visibility revision, read together in one coordinator read and checked again before the job starts, handed to the Authorize hook. Admission sees AdmissionInput::fork (ForkAdmission) and is charged the exact bytes of the packs the fork inherits (a source it would refuse is refused before admission), once and in the batch that creates the job, so quota is a hard bound: an exhausted window refuses the fork before any work. The reservation lives as long as the job (24 hours); the final batch commits the Committed outcome and the replay record. The destination must not exist beforehand: the fork registers it. The remote hook schema gains Operation.fork (field 9) and AdmitRequest.fork (field 8), additive. On Workers, mkit_server_worker::fork::with_fork_timer registers kind 16 on a Paid deployment's namespace coordinators; an embedder that exposes fork_repo MUST give the timer the same takedown_denial and extraction threshold as its pipeline, and MUST apply the storage-lease executor and lease-recovery modes before starting a fork. Under the default admission the fork is also counted against the namespace's aggregate cap, in the job-creation batch. The pack-set plan runs once on the request, before admission (up to 600 storage calls, whatever admission then decides), and an abandoned job on a Workers plan without the kind-16 timer (Free) expires only when a request polls it. New public items: fork::{ForkRequest, binding}, pipeline::ForkAdmission, fork::SettleV1::{declared_bytes, namespace_cap}, fork::ForkError::Quota and fork::Failure::OverAdmitted. OpKind::ForkRepo gains source_visibility_revision; a job row written by an earlier build without SettleV1::declared_bytes does not decode (pre-release rows only).
  • All-parent history continuations in canonical timestamp order. An embedder drives page 1 with per-commit read_canonical_in on a selected-ref reader and session, feeding mkit_core::history_order::TimestampDiscovery, then ObjectReader::issue_history_continuation_in mints a version-2 binary continuation from session memo evidence and live fences alone — no object or storage proof calls. walk_history_page_with_options_in with ContinuedHistoryOrder::TimestampDiscovery redeems pages 2+ on fresh readers and sessions without replaying page 1, hydrating unknown priority keys in batches inside the existing read envelope. History state caps (frontier, emitted set, witness, payload) report the new typed HistoryStateLimit error in both public and owner views. walk_history_page_in first-parent paging is unchanged and mints version-2 tokens.
  • Workers wire CI bypasses the local dev HTTP proxy using a direct workerd socket, with the same pinned runtime, Worker builds, phases and assertions. Existing replay-safe retries remain; any retry emits a CI and summary warning. Recovered-loss diagnostics remain downloadable on successful runs. Direct fixture requests close their HTTP connections to avoid idle socket reuse; signed request bytes and case concurrency are unchanged. Concurrent ref CAS racers follow the documented aborted backoff policy with one signed identity; exactly-one-winner and final-value assertions remain unchanged.
  • Continuation credential tests exercise real signed requests, grants, captured headers, trusted audiences, and anonymous/owner view changes. The Workers embedding guide now covers optional 45-ID batches, bounded path/history helpers, and scoped first-parent continuations.
  • Opt-in server namespace_mode = authority and Worker NAMESPACE_MODE=authority accept opaque lowercase namespace names, including UUIDv7 names. Startup requires Multi addressing, an Authority hook, authority fencing, and any. The deployment supplies ownership; owner grants, grant epochs, and visibility statements are refused. Dedicated authority keys can use * scopes. Hook writer allowances enable storage counters and owner-view object reads and URL issuance. mkit_server::CallerView is public for in-process authorizers. Default self-certifying deployments and the mkit CLI retain their behavior.
  • Authority fencing bootstraps before the first write. Under any, GetAuthorityGeneration and SetAuthorityGeneration no longer require the namespace record; in authority namespace mode a statement (including generation 0) registers a namespace and writes, uploads and visibility changes for an unregistered namespace are refused with namespace not registered. The fenced namespace-creation batch now carries the ag guard, so a stale Authority generation creates nothing. The Get/Set-before-first-write change also applies to self-certifying fenced deployments under policy any.
  • A fenced BeginUpload retried after a generation bump replaces its stale-generation ticket in one guarded batch instead of answering moved until expiry: the old reservation gets an Aborted (EPOCH_MISMATCH, BeginUpload) outcome, counters and the expiry timer are cleaned, and the old multipart session is aborted after commit. The old admission charge is not refunded. No wire or stored-format change.
  • Server readers add walk_history_page_in for retryable, scoped first-parent history continuations. Dedicated MAC keys bind the repository/view, verified credential scope, strict ref/publication fence, live security boundaries and fixed expiry; each page retains fresh permission, source and denial checks. PipelineConfig.history_tokens and the optional Worker HISTORY_TOKEN_KEYS setting enable it. Existing reader methods keep their behavior. Visibility writes increment a retained coordinator revision to fence same-clock visibility changes; existing stores need no reset. Replays rerun live checks and allocate no paging state. Temporary paging proofs are discarded even on cancellation. The default paging regression covers a 40-commit history; the full 201/302-commit fixture runs explicitly in the serial slow CI lane with all assertions retained.
  • Server readers add opt-in selected-ref sessions: ObjectReader::with_selected_ref narrows session canonical and metadata reads to one ref's authoritative anchor — the owner view's live ref, the public view's published head — plus the post-activation security digest, retained as an immutable CaptureCheckpoint. Selected sessions prove targets before locating them in either view and never fall back to all-ref root discovery; every proof reset, expiry, rebind or helper capture invalidates the checkpoint. Session proofs record parent and tag-target roles and decoded commit/remix kind and timestamp only under a live checkpoint, as structural evidence for history continuation issuance. selected_capture_in starts a page-1 loop; unscoped sessions, non-session reads and issue_urls are unchanged.
  • Server readers add walk_history_in, locate_commit_in and read_commit_path_in for bounded selected-ref parent discovery and exact decoded paths. First-parent and all-parent traversal are explicit; merge frontiers, tag peeling and path depth are bounded. The request session retains all accounting and live view/authority/denial checks. This fixes old-history acquisition budgets without claiming a latency target. Optional path witnesses supply output-budgeted commit/tree bytes for local proofs. Existing reader APIs and URL target grammar are unchanged.
  • mkit-client (formerly mkit-push): a bounded async ticketed push primitive for native and wasm clients, with injectable HTTP transport, signer and clock. It shares canonical transport messages, uploads parts where necessary, polls pending verification and exposes conflict/replan outcomes. Staging, persistence, scheduling and policy remain host-owned; the CLI push path is unchanged.
  • mkit-rpc/transport-messages exposes the same generated messages without Connect bindings or tokio. Existing transport consumers keep their APIs.
  • Portable conformance cases publish empty files, deep trees and byte-distinct Unicode/case paths through the transport and read them through HTTP. Core lookup covers a 2 KB path; HTTP keeps its 1024-byte decoded-path contract. Shared deadline cases complement the wasm entry-point runtime regression. A signed takedown contract checks public, private-token and owner denial before and after restart. The existing multipart binary fixture adds HEAD/range checks.
  • Public constructors cover every sink-visible OutcomeKind and ReservationV1 result, including repository storage changes; Outcome::new constructs delivery payloads without stored rows. Types retain their non-exhaustive annotations.
  • Pipeline::repo_storage_many reads up to 100 repositories in one namespace with one coordinator get_many, including per-repository owner authorization. Missing and unauthorized repositories both yield None. The Worker embedding exposes the method and MAX_REPO_STORAGE_BATCH.
  • Embedder documentation explains durable sink retries, unordered at-least-once delivery, and admission backpressure only above the configured backlog cap.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Exact per-repository stored-bytes accounting for multi-repository deployments: the sum of the sizes of the distinct packs that are members of a repository (a pack shared by two repositories counts in each). A per-repository counter lives in the coordinator, created with the repository, and a pack is counted exactly once, when its consumption first reaches the coordinator, including consumption on different branches under D34 and consumption by implicit session writes. The counter is eventually consistent and exact. Every change queues a new OutcomeKind::RepoStorageChanged { stored_bytes, version } outcome (absolute value, monotonic per-repository version; hooks proto Outcome.repo_storage_changed, field 11) through the outcome sink, and Pipeline::repo_storage reads { stored_bytes, version } in one coordinator call, authorized as an owner read. Counting is part of relay delivery to a coordinator, so every RelayHandler counts. ReservationV1 gains a RepoStorageChanged state, so exhaustive matches must handle it. A store created before this change has repositories without a counter and must be reset.
  • SetRepoVisibility now runs the embedder's Admission hook and records an outcome for the OutcomeSink, in both the owner-signed envelope mode and the statement mode, like every other mutating RPC. A refusal or challenge changes nothing; a granted reservation is settled in the same atomic unit as the visibility row and its listing index. Hooks recognize the change by input.op.procedure() == Procedure::SetRepoVisibility (with OpKind::SetRepoVisibility { visibility }, declared_bytes = 0), and DefaultAdmission charges nothing for it. Remote hooks send the procedure and visibility in additive optional Outcome fields. Pipeline::set_repo_visibility_with_meta also returns the admission's receipt headers, which the Connect service now sets.
  • Server embedders can share ReadLimits and ReaderSession across canonical and metadata reads to cap aggregate calls, decode work, encoded I/O and output.
  • mkit-core::history_order adds a pure, I/O-free timestamp/discovery-order all-parent history reducer shared by the server and embedders (TimestampDiscovery, HistoryOrder::TimestampDiscovery). Decreasing canonical Commit.timestamp/Remix.timestamp keys break ties by retained discovery order — never a hash tiebreak, never a topological gate. The pending frontier is capped at 256 verbatim slots and the emitted set at 192 ids; exceeding either bound fails explicitly without truncation, eviction or re-emission. Dedup goes live with the first revisitable emission and decoded is distinct from emitted, so a page can snapshot after the last output's parent enqueue and resume byte-identically. Seeding closes at the first completed emission (WalkStarted) so a late seed cannot force a re-emission. A strict little-endian snapshot encoding (encode/decode) carries the full state for continuation issuance. The order, caps, duplicate-slot policy and snapshot format are normative in docs/specs/SPEC-HISTORY-ORDER.md. Existing traversal APIs — the server's BFS HistoryMode::AllParents and the CLI's date-order log — are unchanged.
  • Production verification timer failures and races report a redacted error class, proposed next delay, and next attempt through existing telemetry. mkit_server_verification_timer_failures_total counts fires by outcome and class. Backoff, timer outcomes, and test-faults telemetry remain unchanged; wipe: no.
  • Client addressing of authority-mode namespaces (SPEC-SERVER §6.2.2, SPEC-TRANSPORT-CONNECT §7.4): mkit+https:// and loopback mkit+http:// remote URLs now accept the union of the self-certifying grammars (ed25519-<64 lowercase hex>, 0x<40 lowercase hex>) and the opaque authority-mode grammar [a-z0-9][a-z0-9._-]{0,71} other than root, ed25519-… and 0x…, so a mkit+https://host/<uuidv7>/<name> remote clone, pulls and pushes against a namespace_mode = authority deployment. mkit-core gains repo_identity::OpaqueNamespace, RemoteNamespace and the owned RemoteIdentity (with MAX_OPAQUE_NAMESPACE_LEN), and mkit-server's authority validator delegates to OpaqueNamespace::parse so client and server share the one grammar. mkit-transport-connect adds remote_identity_from_url beside the strict repository_identity_from_url and ConnectTransport::remote_identity(); UrlIdentityError gains Namespace/Name variants that name the offending segment and the accepted grammars. The CLI prints those clearer URL/identity errors, and a plain https:// URL is refused with guidance pointing at mkit+https:// and explaining that plain HTTP serves objects by repository path (SPEC-HTTP-OBJECTS). mkit-client's push Destination accepts the same union. Grants, grant epochs and visibility statements keep the self-certifying grammar and are never built for an opaque namespace: mkit grant, mkit epoch and mkit visibility set --statement refuse such a URL with an error naming SPEC-SERVER §6.2.2, while the envelope form of visibility set works. No wire or server-behavior change; wipe: no.

Fixed

  • History continuation tokens outlive the issuing signed request envelope. Their fixed lifetime is the minimum of the configured token TTL and any write-grant expiry, without the request-local proof cap or the envelope's expiry; successors retain the original absolute expiry. Both first-parent and selected-ref timestamp issuance follow this rule. Each authenticated page still verifies its own currently valid signed envelope, current authority and credential scope, and keeps bounded proofs and fresh live security checks. Embedders may use short request envelopes for history walks. No API, wire, stored-format change, or store reset.
  • Takedown discovery found no ticketed memberships. It accepted only an empty m value (an immediate upload), but every membership written by a ticketed advance carries a clearance witness, so a holder sweep failed with a corrupt-membership error on any repository that tickets uploads and never reached its blocked object. It now accepts a valid clearance witness as a membership and still treats any other value as corrupt. No stored-format change; no store reset.
  • Scheduled verification no longer asks clients to wait up to 60 s. The pending-verification Retry-After hint followed the verification timer's due time, which a failure backoff or lag wait pushes to 20-60 s while the job progresses. Pending answers now carry a 1-3 s hint.
  • Scheduled pack verification plans a fresh 10-second deadline for each bounded inventory write and checkpoints every completed Decode entry. Slow stores and retries retain durable progress; the final bounded facts and cursor share one guarded apply. Inventory expiry and CAS contention now have distinct typed causes and metric labels, with per-attempt staging progress. No stored-format change; no wipe or store reset is needed.
  • Indexed member delta reconstruction descends and reconstructs iteratively, preventing call-stack growth at the configured chain-depth limit. Integrity, denial, membership, and budget checks keep their order; raw member prefix and frame reads remain concurrent. No public API, wire, or stored-format change.
  • Signed reader envelopes now capture credential headers for continuation scope. Changing payment, authorization, or configured credential headers invalidates a continuation; signed reads still do not run admission validation.
  • Workers wire CI retains runtime, server/alarm and request correlation logs when existing retries recover connection losses, including successful runs. The job summary reports retries by phase and case; runtime versions accompany the artifact. Conformance assertions and retry policies are unchanged.
  • Wasm Connect dispatch ignores both client timeout headers before connectrpc computes a deadline, including direct connect::service calls. Configured deadline policies are ignored on wasm; native deadlines are unchanged. Worker adapters share this protection without changing body streaming or auth.
  • Worker conformance growth measurements use a fresh local state directory for each case, preventing earlier replay rows from expiring during ticket-growth calibration. Both growth assertions and the quota suite remain required.
  • Retire the stale Workers operations claim that interrupted takedown activation can replay HTTP 200 with inactive denials. A regression checks denial rows through signed-request and operation-id retries after interruption and restart: both remain HTTP 503 until activation finishes. Runtime behavior is unchanged.
  • The public hook golden-request test expects all 18 requests, including visibility and repository-storage outcomes, and checks that every fixture field survives protobuf decoding. The stale 16-request assertion failed both the macOS and Cloud Build main test gates.
  • Publication limit-reached telemetry counts only newly committed foreground stops that answer unavailable, excluding checkpoint conflicts and input limits. Per-target URL issuance preserves typed authorization errors after a reader cap; only unavailable is rewritten as reader exhaustion. Publication ledger documentation states its preparation-onward scope and recovery exceptions, and names both canonical decode-budget messages.
  • AdmissionInput::new_to_repo_bytes is now the declared pack size or Some(0) when the pack is already counted for the repository, for BeginUpload and streaming uploads under multi-repository addressing (it was always the declared size, contradicting its doc), and its doc states that it is a pre-admission observation. Committed.new_to_repo counted a pack twice when two branches consumed it concurrently; it is documented as an observation and is exact only where the counter is in the writing partition. The RepoStorageChanged outcome is the accounting source.
  • Object-reader cap hits consistently return ResourceExhausted with a stable public message, including delta-depth and external-base caps hit while checking takedown denial; unprovable public IDs retain their uniform absent result.
  • Owner object-reader authorization preserves ResourceExhausted when the caller's storage-call budget runs out during the repository-state lookup.
  • Workers retry transient alarm scheduling failures twice after committed timer writes and return exhausted failures for replay and cold-start repair.
  • The embedded Worker example enables the pure-Rust zstd decoder for default CLI pushes.
  • A prepared publication proof is bound to the publication row it was computed against (membership generation, sequence and deletion boundary). A generation change, a deletion boundary or a same-pair advance between preparation and the final apply now refuses with retryable unavailable instead of committing the proof onto the newer row. The binding is request-local; no stored field changes.
  • Publication verification draws preparation, dependency visibility and every optimistic retry of the final denial proof from one request allowance (with headroom reserved for settlement; snapshot, lease, checkpoint and commit calls are counted too), and exhaustion is unavailable with the message publication verification capacity exhausted whichever call it lands on. It was invalid_argument (object index limit exceeded) in some phases, which told clients the content was permanently invalid. A resumable job's recorded stop at an unsupported historical limit now answers unavailable with publication verification limit reached and a counter, instead of invalid_argument. Per-lookup index caps and the indexed decode budget keep their specified errors on both paths.

Performance

  • Request reads use bounded waves for ref listing, sealed inventory, and contiguous raw members in mixed raw/delta batches, fresh dependency denials, and publication dependency rechecks. Lease reads, implicit packmap prologues, and pure continuation reads overlap independent work. Ordered result handling retains refusal and uniform-absence precedence; unknown or tight allowances retain serial admission. Stores can expose remaining read-call and encoded byte allowances through additive hidden trait methods; their default is unknown. No wire, stored-format, limit, or store-reset change is required.
  • Scheduled pack verification stages Decode inventory in batches. Up to 16 decoded entries share one inventory read (head and entry rows together) and one guarded apply (at most 83 of the 100 operations), instead of three remote calls per entry; an entry with paged references still stages alone. Denial reads stay one per object (content shards partition objects, so ids of one batch never share a read) but now follow the batch's apply instead of preceding each entry's: a block that lands while a batch is in flight is refused (Blocked), where reads taken first would have left every earlier object of a batch a wider window than one entry. The only visible difference is that a blocked pack's inventory rows are written before the pack is refused. Remote calls per entry fall from 4 to about 1.2, so a slice reaches about 177 entries instead of 48: a 500-object push needs 3 Decode slices instead of 11 and a 3,000-object push 17 instead of 63 (slow-store model: 177.7 s to 103.0 s and 1,007.7 s to 559.8 s). Per-entry checkpoints, restart cursors, per-apply deadlines and first-occurrence inventory digests are unchanged; a crash between a batch's apply and its checkpoints replays the entries and finds their rows already staged. verification_inventory_progress gains fixed per-slice attribution fields (slice_calls, inventory_batches, window_read_ms, decode_ms, denial_ms, denial_calls, write_ms, checkpoint_ms) on the existing console channel. Stored formats are unchanged (wipe: no).
  • Scheduled pack verification looks owed closure children up in batches instead of one child per alarm fire. Up to 90 owed ids share one locate_split per slice, with the per-slice reserve applied per batch; a batch that outgrows a slice halves its persisted cap and retries, and a single id that cannot be answered in one slice still ends ClosureCapped. Closure semantics (open closure, lag window, capped answers) are unchanged. A push whose tree names 500 existing files drops from 10,735 to 427 storage calls (639 s to 25 s modeled); 3,000 files from 63,450 to 1,438. Stored formats are unchanged (wipe: no); jobs written earlier keep their smaller cap.
  • Scheduled verification of small packs finishes in roughly half the time at 50 ms per storage call. The Extract phase runs its selection steps (scan, closure, dependencies, members, head, select, choose) back to back in one slice and one guarded commit, bounded by 16 steps, 64 remaining remote calls, a commit batch within the apply limit, and 2 s of slice time; effect stages (upload, completion, holder enqueue, delivery) still end their slice. A step that fails after earlier steps in the slice keeps those steps and reruns first in the next slice, so every verification outcome, error and persisted state is unchanged. The relay-delivery wait polls after 300 ms and then every second instead of every two seconds (the relay's wake nudge is unchanged). SliceLimits::extract_chain_steps (default 16; 1 restores one step per slice) and with_extract_chain_steps are new. No stored-format change: wipe not required.
  • Scheduled Extract decodes staged, local, non-delta member frames in the call that used to stage a member-frame row and yield, then walks several chunks per PREFLIGHT and UPLOAD slice instead of two slices per chunk. A slice keeps fusing while the chunk fits the staged non-delta fast path, the previous chunk finished inside the slice without ending a part, and the shared step, remote-call, operation and time bounds hold; a chunk that fails after earlier chunks in the slice keeps those chunks and reruns first in the next slice. Each chunk's own frame denial read stays per chunk; each source pack's denial is read once per slice, after its last chunk and before the part upload and the commit, so a block landing mid-slice still refuses the whole extraction. The source lease is still renewed once at slice start and again immediately before every part upload, and holds and pending-holder protection still run once per slice. On a 64-chunk object of ~256 KiB chunks, remote calls per chunk across both stages fall from 24.5 to 3.1 — PREFLIGHT from 131 slices and 786 calls to 6 slices and 101, UPLOAD from 131 and 785 to 6 and 100 — and remote plus pack-window calls per chunk across both stages from 26.6 to 5.2. At 150 MiB (600 chunks) the two stages drop from 16,079 remote+window calls to 3,155 (804 s to 158 s modeled at 50 ms per call); at 1 GiB (4,096 chunks) from 109,809 to 21,499 (5,490 s to 1,075 s). Pack window reads remain one per chunk. External and delta members keep their separate descent, member-lookup and base-resolution slices, and extract_chain_steps = 1 restores one chunk per slice. Stored formats are unchanged (wipe: no).
  • Fewer sequential storage rounds per read, with no change to results, limits, guards. Backend R2 calls drop; KV calls and ledger charges drop only by the removed publish-time block read (one per proof-expanding decoded object; a nine-file show with sizes goes from 72 to 69 KV calls, with fewer for a no-sizes show and a six-file show). A rare failure path, where the pack prefix exceeds the decode budget before the frame read, can now send and charge the frame read. The Workers R2 adapter serves a bounded blob range with one ranged GET instead of a HEAD followed by a GET (a missing object is still absent, a start past the end is still unsatisfiable, a malformed range is still invalid, and the read ledger still charges two units). A public reader's first root enumeration scans the sharded ref index in reserved waves of the shared six-call envelope rather than one shard per round. A raw member reads its pack prefix and frame together, a history page re-checks its nodes' source packs and final guards in the same round. A duplicate block-row read at publish time, which asked the final guards' question again, is removed. Every authorization, membership, denial, source and final fresh check keeps its boundary. In the call/round model, a nine-file show with sizes goes from 59 to 34 rounds, a five-commit log from 54 to 34, and a fifty-commit log from 621 to 380.
  • Borrowed server stores forward batched content-index reads to the backend. Two-key denial probes use one batch, and member dependency checks use one batch each for the object and its pack. Fail-closed checks remain live.
  • Server readers share located member facts and sealed inventory within each batch, coalesce repeated denial checks, and bound independent scans, membership reads and raw canonical loads with one shared six-call I/O envelope. Scan-prefix pagination and deterministic first-member selection retain their existing caps. Strong denial directories and final access phases remain fresh per operation. Waves reserve inherited backend budgets before dispatch; fallback graph walks recheck an ancestor's stop after loading before exposing its children. Read credits are scoped to their owning wave; unrelated work and cancellation of another wave cannot spend them. Multipart and root-pinning mutations isolate their internal probes from active read credits. Public views preserve backend served prefixes so continuation is admitted by the reader. Raw-load waves shrink for large encoded frames to retain the previous single-member transient memory allowance alongside earlier canonical results. Bounded history and commit/path reads share that admission envelope across the complete operation, reuse actual source locations and sealed facts for denial, and retain fresh final page/witness guards after callbacks and descriptor I/O. Final authorization and proof revalidation precede live descriptor checks and fresh source guards, including singleton history/path results and retained pages or witnesses, so callbacks cannot reuse earlier indirect chunk clearance. Canonical and metadata batches also refresh final guards after takedown callbacks, so a durable source block installed by a callback remains absent.
  • Opt into canonical/metadata batches of up to 45 ids with ObjectReader::with_batch_limit(45)?; the 16-id default, URL issuance cap and all accounting allowances remain unchanged.
  • Keep one reader/session per operation, supply canonical parents before children, and request metadata only when lengths are needed. Parallel raw-member reservations count against session allowances.
  • Server reader sessions capture roots once and reuse bounded local graph proofs across canonical and metadata batches. Retain the same reader/session through a request; start a new session to observe new commits before proof expiry. Reachability-lag expiry captures fresh roots without resetting budgets or the request deadline. ReaderSession::with_deadline accepts an earlier absolute request deadline. Scope changes discard proofs without refunding budgets; authorization, membership and denial remain live. No persisted cache or writer proofs in the shared published cache.
  • Workers deployments can enable REPO_INDEX_PREWAKE=true to activate all sixteen repository index objects concurrently with signed-read authorization. REPO_INDEX_PREWAKE_TTL_SECONDS defaults to 90 and accepts 1 through 120. The option defaults off, uses a bounded volatile cache and a separate sixteen-call warm-up allowance inside the existing request budget, and suppresses failures. Anonymous reads and single sharding skip the wave. No public wire, stored-format, or store-reset change.

Changed

  • Inventory staging reuses the entry lookup for entries without history references, retaining the fresh guarded entry/head apply, replay handling, denial checks and durable verification cursors. No public API, wire, hook, Outcome or stored-format change; no store reset is needed.
  • Core merge, revert and cherry-pick use ObjectSource + ObjectSink; merge-base, ancestry and ancestor-set walks use ObjectSource. CLI behavior is unchanged. MemoryOverlay provides immediately readable outputs with explicit aggregate read and output budgets, and the wasm gate executes the generic operations.
  • The paid-profile reference fixture uses neutral embedding names throughout code, scripts and CI. Use apps/embedded-worker/tests/embedding-conformance, the admin harness flag --embedded, cases embedding.public_fixture and embedding.multipart_file_readback, markers MKIT_EMBED_* and the fixture switch FIXTURE_OUTCOME_FAIL. Update local example routes to /_embedding/mkit/ and /_embedding/operator. Acceptance coverage and embedder APIs are unchanged.
  • Scheduled verification emits correlated upload, job, alarm, phase, delivery, readiness and ref-commit telemetry through the Worker console. Relay delivery nudges a bounded set of waiting timers while retaining the recovery poll. Pending retry hints reflect observed durable timers, rounded to 1–60 seconds, with matching typed details. Verification semantics and embedder APIs remain unchanged; timer handlers may optionally expose their metrics sink for commit-confirmed checkpoint observations.
  • Stored formats are not a compatibility contract before 1.0; the golden fixtures that pinned v0.5.0 row encodings are removed and a store may need a reset across versions (SPEC-SERVER §17).
  • Documentation: SPEC-SERVER §9.3, §10.2 and §18 clarify the integrity frontier that verified members provide, the pair-coverage and denial obligations it does not waive, and the duty to document historical-support limits. The Workers operator guide gains a table of the selected publication verifier paths and their real limits, and the rules for enabling takedown (rehearse on a copy of an existing store) and inspection (empty store). New regression tests pin the baseline fail-closed behavior of every selected path. No production behavior changes.
  • [embedder: breaking API] ObjectReader::object_metadata now fails the whole batch with ResourceExhausted when a proven object's delta depth or external base lookup cap is hit, matching read_canonical (previously the ID was silently absent).
  • Documentation only: stale Workers operator and README claims (reader cache refresh, supplied hooks needing remote configuration, the absent ListRepos, decoder scratch in the launch graph, typed read exhaustion) now match the code; SPEC-SERVER §18 is deployment-neutral with the Workers launch rules in the Workers operator guide; the store::inspection_* modules and the deferred inspection, Event and proof work are marked as not integrated or not implemented; SPEC-WRITE-GRANTS §9.4 requires server-clock visibility change times independently of statement anti-replay ordering.
  • The pack-ruzstd feature relies on a bounded-decode patch to ruzstd 0.9 tracked by KillingSpark/zstd-rs #124, "Refuse blocks that decode past Block_Maximum_Size", still open. Crates.io consumers must apply it in their own workspace until a release includes it: [patch.crates-io] ruzstd = { git = "https://github.com/officialunofficial/mkit", tag = "v0.5.0" }. Upstreaming is in progress.

Documentation

  • Integrator note on history continuations: captured credential headers (names and values) bind a token, so a renewed Authorization, Payment-Authorization or PAYMENT-SIGNATURE value mid-walk ends the continuation; keep them stable or do not send them on history reads. Anonymous readers carry no credential digest, and the fixed token lifetime is the minimum of the TTL and any write-grant expiry. Added to the server spec and the Workers embedding guide.
  • The generic Workers reference composes in-process hooks, bounded request reader sessions, URL tokens and a durable outcome/storage projection. Its separate local harness verifies cold retries, duplicate/out-of-order delivery, bounded deduplication retention and purge. The receiver keeps the newest 1,024 accepted reservation IDs and preserves its highest-version storage projection. The Workers embedder guide documents budgets, continuation and per-pin upgrades.
  • Align the server profile map and invariant enforcement index with implemented publication, synchronous inspection, denial, preservation and admin behavior. Retain async-inspection storage groundwork and distinguish future integration.
  • Track the bounded ruzstd patch in KillingSpark/zstd-rs #124, "Refuse blocks that decode past Block_Maximum_Size". Embedders still require their workspace [patch.crates-io] until a release includes the fix.
  • The Workers operator guide gains "Diagnosing slow and cold reads": enable Workers automatic tracing, count distinct Durable Object IDs per trace, and compare cold and warm spans. It lists the expected distinct-partition counts per read shape, the Durable Object idle eviction window and the one-time effect of NAMESPACE_LOCATION_HINT. The read-rounds test probe now also reports distinct partitions per read by kind. No production behavior changes.

CI

  • Four heavy indexed extraction and cold-delta regressions use exact-name, serialized nextest overrides with a 300-second ceiling. Their full fixtures and assertions remain in the default and unchained CI runs; the global timeout stays unchanged.
  • The indexed verification suite runs a second time with one Extract step per slice (MKIT_TEST_EXTRACT_CHAIN=1), so every outcome assertion holds both chained and unchained.
  • Hosted Linux gates run the isolated pure-Rust scheduled decoder heap bound. Workers CI builds the locked embedding wasm fixture, checks both patched launch graphs, runs wasm decoder regressions, and requires paid indexed publication, URL-token reads, supplied hooks, cold outcome retry and injected slice recovery.

Breaking (toward 0.6)

  • [embedder: breaking API] The ALPHA decision facts now model the exact ticket and pending-reservation observations. Migration: replace TicketFact.authority with authority_generation: Option<u64>, replace upload with upload_session: Option<Vec<u8>>, and supply the reservation-derived ticket identity. Replace ReservationFact::Pending.operation/expires_ms with created_ms, reconcile_ms, and op: ReservationOp; retain operation identity in the request. Add storage: None to Snapshot and an empty storage vector to AcceptanceEffects when counting is disabled. Handle GuardKind::Storage and ReservationTerminalKind::StorageChanged in exhaustive consumers. Wire and stored formats are unchanged; no store reset is required.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Fork jobs retain the authorized source policy, exact authority observations, and accepted result. Phase::Accept separates guarded acceptance from published clearance. Acceptance atomically saves replay and settles the reservation; activation never settles it again. Destination writes stay blocked until clearance completes, and failed destinations stay inert. Migration: use start, start_with, or start_planned rather than job literals, handle the new phase in exhaustive matches, and reset pre-production stores before using this format. Source changes after admission use uniform not_found instead of the former visibility-change retry response.
  • Unstable CLI library API: remote_dispatch::open_trusted returns Remote instead of Arc<dyn Transport>. Migration: use Remote::with_source to lend the configured source for a complete repository operation. Existing public transport-based push, fetch, and pull helpers remain available. Published client and transport APIs are additive; no stored-format change or wipe.
  • [embedder: breaking API] Raw ContentIndex::{block, install_block_action, install_stored_block_action} writers require a DenialCapability obtained through Pipeline::denial_capability() with takedown_denial=true. Takedown services and work handlers default to denial disabled and refuse writer operations with failed_precondition and takedown denial disabled before any write. Migration: pass the pipeline's capability through with_denial_capability to takedown::{Service, work::Work, late_owner::LateOwner} and to raw writer calls. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] reads::ReadAccess::new takes a final takedown_denial argument. Typed object lookup and canonical member reads use the same denial gate as pipeline reads. Migration: pass the deployment's exact mode, or use Pipeline::read_access() to inherit it. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] indexed::job::VerifyTimer now requires a takedown_denial field, and indexed::verify::{verify_ticketed, verify_ticketed_inspected} take a takedown_denial argument immediately after IndexedConfig. Migration: pass the same value as PipelineConfig.takedown_denial to every scheduled and inline verifier. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] Workers adapter::{timer_registry, timer_registry_with_blobs} take a final takedown_denial argument, and verify::with_verification_timers takes the mode after authority_fence. Migration: pass the same mode as the validated pipeline configuration so timer reads use the deployment's denial policy. The rooted production alarm registry separately receives the pipeline's denial-write capability. No wire or stored-format change; wipe: no.
  • The unstable mkit_cli::remote_dispatch::push_branch_steps callback now returns mkit_sync::PushError. Migration: return typed sync/push failures from the callback; the wrapper still returns DispatchError to its caller.
  • The unstable mkit-cli library's PushControl gains an observer field. PushControl, StepAuthority, and push planning now belong to mkit-sync; the CLI retains its re-exports and maps typed library errors to its existing errors. Migration: construct controls with PushControl::default(), or add observer: &NoopObserver to struct literals. Set the observer for native sync calls; CLI compatibility wrappers retain CLI progress and cancellation. Repository callers can use mkit_sync::push with PushOptions and match PushError::Sync(SyncError::Source(_)) for backend failures. CLI output and exit codes are unchanged. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] mkit-sync::RemoteSource read methods now return backend-neutral SourceError; SyncError::Transport is replaced by SyncError::Source, and ErrorClassifier receives &SourceError. Contextual RepositoryNotFound and AdvertisedPackMissing errors gain a source cause; PackChainInvalid gains an optional source cause. The CLI library's public DispatchError gains Source and retains the same contextual causes: RepositoryNotFound and AdvertisedPackMissing gain source, and PackChainInvalid gains an optional source. Migration: update source implementations and exhaustive error matches; adapt core transport failures with SourceError::from. Update CLI error constructors and matches for the new variants and cause fields. No shims, wire or stored-format change; wipe: no. Pending verification, membership lag, retry hints, payment challenges, and signing failures remain typed. Raw Connect failures retain code, message, details, status, and response headers. SourceError::class() separates refusal, transient failure, and resumable host action; sync never sleeps or retries these outcomes. Core's non-exhaustive TransportError gains PendingVerification, MembershipNotVisible, and RetryAfter for host-driven transports; existing transport retry behavior is unchanged. ClientSource gains a defaulted observer parameter and accepts observed signed clients. Existing CLI error output and exit codes stay unchanged; no printed-text changes.
  • NativeHttp now defaults to 30 seconds for metadata and 300 seconds for UploadPack, UploadPart and DownloadPack, including response receipt, instead of ten minutes for every request. Migration: set the independent timeout builders, or with_timeout, when a longer bound is required. Supplied user-agent and gzip/identity-compatible accept-encoding headers now survive dispatch. Unsupported or malformed encoding negotiation is refused before connecting instead of overwritten. Migration: request only gzip/identity codings. No stored-format change or store reset is required.
  • [embedder: breaking API] Embedded dispatch header callbacks now require MaybeSync (Sync on native targets), allowing native dispatch futures to be Send. This covers Pipeline::{dispatch_embedded, dispatch_fork} and the Workers adapter's EmbeddedDispatch::{dispatch, dispatch_fork}. Migration: use a thread-safe callback on native targets; replace erased &dyn Fn(&str) -> Option<String> with &(dyn Fn(&str) -> Option<String> + Sync). Concrete Sync callbacks and wasm32 callbacks need no changes. Dispatch behavior and order are unchanged. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] mkit-push (never published to crates.io) is renamed to mkit-client; there is no shim. Migration: depend on mkit-client and rename mkit_push to mkit_client. The old names (Push, Plan, Entry, Limits, Destination, HttpTransport, Signer, Clock, Error, proto) are unchanged. Outcome, PackmapMode and Error are now #[non_exhaustive], and Error gains PaymentRequired, PendingVerification, MembershipNotVisible, RetryAfter, Sink and Signature: add a wildcard arm to any exhaustive match. An HTTP 402 (or permission_denied with an admission challenge) was Error::Remote; it is now Error::PaymentRequired, and a body that is not a Connect error no longer fails as Invalid("Connect error JSON") when the status is 402. Push::run still polls pending and membership-lag answers itself. Pack bytes and ids are unchanged. No wire or stored-format change; wipe: no.
  • [embedder: breaking API] Generated AdvanceRefsRequest and remote-hook Operation Rust records gain the published-source fields. Migration: build generated messages with ..Default::default(); keep the new fields absent for existing calls, or select the source form described above. No store reset is required.
  • [embedder: breaking API] SliceLimits gains extract_chain_steps (default 16). Migration: add extract_chain_steps: SliceLimits::default().extract_chain_steps to struct literals, or construct with SliceLimits::default() and the with_extract_chain_steps setter. No stored-format change; wipe: no.
  • [embedder: breaking API] [embedder: stored-format change] A completed fork flags the membership witness of the packmap head it inherits. store::publication::Witness has a new boundary: bool field (a twenty-byte encoding when set; the nineteen-byte encoding of every other witness is unchanged and existing rows keep decoding). Migration: add boundary: false to any Witness { .. } literal. A packmap node whose membership value is not a valid clearance witness now fails publication verification as unavailable (it was accepted as present). New coordinator rows fj (fork job) and fo (fork id sets) and timer kind 16 are additive; no store reset. Procedure::Fork, OpKind::ForkRepo and StoredProcedure::Fork are additive on non-exhaustive enums. StoredResult is exhaustive, so its new Fork variant is [embedder: breaking API]. Migration: add a Fork (or wildcard) arm to any match on StoredResult. New public items: mkit_server::fork, takedown::inventory::{SealedFacts, sealed_facts, parent_page} and takedown::denial::require_pack_clear_sealed. pipeline::ShardMap gains a defaulted object_index_partitions (empty by default); a custom map that serves forks must list its repository index shards.
  • [embedder: stored-format change] [embedder: store reset required] Each repository's index is now sixteen shards (the top four bits of the object or pack id) instead of 4,096, so a repository's rows, copies and sweeps touch at most sixteen partitions. Content shards (4,096, global), the coordinator and the ref and ref-index partitions are unchanged. store::keys::LAYOUT_VERSION is now 2 and the Workers SQL schema version 3; a store at any other version is refused at store open (Workers SQL), pipeline construction (implicit-layout stores), the first write, snapshot restore and export import. A custom store that keeps an explicit v row is not checked on reads before its first write, so a reset is mandatory. Refs-only stores report the new version, so their earlier exports are refused too. GetServerInfo.index_fanout now advertises 16 and store::REPO_INDEX_FANOUT is new; store::INDEX_FANOUT keeps meaning the content-shard fan-out. Migration: reset the store; there is no in-place upgrade. Per-Durable-Object scaling limits are in docs/operations/workers.md. The same layout change gives sealed pack inventories a separate dependency range (external delta-base placeholders, with a count and digest in the inventory head), read through the new takedown::inventory::visit_dependencies under the same seal check as visit.
  • [embedder: breaking API] History continuation tokens are a fixed-order binary encoding under purpose mkit-history-continuation:v2; version-1 JSON tokens are rejected. First-parent paging whose provenance chain exceeds 1,024 commits now fails with an explicit HistoryStateLimit::Provenance in both public and owner views, replacing the previous exhaustion/absence behavior. The bound itself is unchanged in effect: the version-1 ancestry claim carried the same tip-to-cursor chain under the same 1,024 cap and stopped at the same depth, silently for public readers. Migration: restart paging from page 1 — walk_history_page_in mints version-2 tokens, and the new issue_history_continuation_in mints timestamp continuations from a sealed TimestampDiscovery walk. No stored-format change.
  • [embedder: breaking API] http_mount::early_object_error now requires the deployment namespace mode, so authority-mode HTTP object routes pass the early gate. Migration: pass the deployment's NamespaceMode as the third argument, for example early_object_error(method, url, cfg.namespace_mode) with cfg: WorkerConfig. No wire or stored-format change.
  • [embedder: breaking API] AuthorityKey.namespaces becomes scope: AuthorityScope. Migration: wrap exact names in AuthorityScope::Exact, parsing each with the deployment's NamespaceMode::namespace, and use AuthorityScope::Any only in authority mode. AuthorityFence::new and parse keep the default mode; use new_with_mode or parse_with_mode for authority-owned deployments.
  • [embedder: breaking API] AuthorityStatement.namespace and HTTP ParsedUrl.repository use the server's mode-aware namespace types. Migration: use namespace key() for storage routing and identity name(), namespace(), and Display for selectors. Use route::parse_with_mode for authority routes; route::parse retains the default grammar. The core self-certifying namespace and identity types remain unchanged. These API changes add no stored format or reset requirement; pre-production stores may be reset when changing deployment trust models.
  • [embedder: breaking API] On wasm, connect::service returns connect::ConnectService, a protected Tower service, instead of raw connectrpc::ConnectRpcService. Use the new type in explicit annotations and ConnectService::new(connect::router(...)) for custom mounts. The limit, compression and interceptor builders remain available; deadline policies are explicitly ignored. Native types are unchanged.
  • [embedder: breaking API] StoreError adds OperationLimitExceeded for bounded in-memory operations. Migration: handle this variant in exhaustive matches and discard or publish partial overlay writes according to host policy.
  • [embedder: breaking API] Merge, revert, cherry-pick, merge-base and ancestry operation signatures now infer source/sink types. Migration: pass &ObjectStore directly when using store wrappers (for example, boxed_store.as_ref() instead of &boxed_store), and qualify function items with ::<ObjectStore> when no call/expected type provides inference. Direct &ObjectStore calls and explicit ancestor-set hasher arguments remain valid.
  • [embedder: breaking API] Request reservation variants require StoredProcedure. Pass the operation directly to ReservationV1::{pending, committed, aborted, read_served}; with_procedure is removed. Storage-counter events remain procedure-less, and expired upload tickets still report BeginUpload.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Stored visibility requires the server-clock changed_ms; the creation-time fallback and visibility_changed_ms accessor are removed. Read changed_ms directly and retain last_created_ms for statement anti-replay ordering. Reset unsupported stores; no stored-row conversion is provided.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Workers SQL installs the current tables, timer index and schema marker in one transaction, and refuses noncurrent or incomplete stores. Replace internal schema::migrate/migration-list usage with schema::initialize/BOOTSTRAP; StoreMaintenance::migrate remains an initialize/check adapter. Reset unsupported stores rather than upgrading them.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] Workers purge resumes only current four-byte checkpoints (or an empty initial checkpoint), and refuses populated roots without an addressing marker. Reset unsupported state; fresh housekeeping-before-marker bootstrap remains supported.
  • [embedder: breaking API] Unsupported HTTP ?proof=1 queries now return 416 after normal access, published target resolution and takedown checks, before proof preparation, validators or read Admission. If-None-Match: * no longer produces 304. Pipeline::serve_http_object_with_proofs, HttpSeams::proofs, http_objects::{ProofServer, PreparedProof, ProofSource, UnsupportedProofs} and HttpObjectsConfig::{max_proof_content_bytes, max_proof_bundle_bytes} are removed. Migrate to ordinary serve_http_object or serve_http_object_with_runtime; embedders needing disclosure bundles build them with mkit_core::verify::build_disclosure_from and ObjectReader. Generic proof formats, core builders and client verification are unchanged.
  • [embedder: breaking API] Storage layout modules store::{keys, codec, index, tickets, outbox, publication, watermark} move behind the doc-hidden store::adapter_spi. Adapter/conformance implementations migrate those imports to the SPI; embedders import ReservationV1, OutcomeRef, AbortReason, PendingOp, StoredProcedure and TicketCaps directly from store.
  • [embedder: breaking API] Core-only store::{read, view} and unused inspection/restore planning are removed from the public surface. Use Pipeline/ObjectReader for verified reads and portable store::{export_partition, import_stream} for maintenance; the existing native fs constructors remain supported. Inspection planning and old restore tests remain private test support with no runtime callers.
  • [embedder: breaking API] Public configuration structs are non-exhaustive, including UploadLimits, QuotaLimits, OutboxBacklogCap, TicketCaps, GrantSettings, LeaseParams, SliceLimits, MemberSourceLimits, RefRule, RefPolicy, HttpMountOptions, HttpReadRuntime, HttpSeams, AuthorityFence and the Workers adapter's configuration types (including HookRoles::default). Replace literals with new, a parser or Default, then adjust public fields; LeaseParams and SliceLimits also provide builders for common overrides. Existing PipelineConfig::new, IndexedConfig::default, HttpObjectsConfig::default and WorkerConfig::from_vars/from_env remain the construction API.
  • [embedder: breaking API] takedown::work::Work is non-exhaustive. Replace runtime literals with Work::new(metadata, serving, preserved, WorkConfig::new(..)); configure purge, discovery margin and acquisition profile on the non-exhaustive WorkConfig before assembly. Routing, retention and clock remain explicit.
  • [embedder: breaking API] The duplicate HTTP admission seam (HttpAdmission, AdmitRequest, AdmitDecision, Admitted, NoAdmission and HttpSeams::admission) becomes crate-private. Implement pipeline::Admission in the HookSet and enable HttpObjectsConfig::admit_reads; HTTP serving and settlement keep their existing behavior. HttpSeams retains its constructor and other public seams.
  • [embedder: breaking API] ObjectReader::object_sizes is removed. Use object_metadata, selecting logical_len for Blob payload lengths and canonical_len for other kinds to reproduce the old mixed-size result.
  • [embedder: breaking API] admin::{plan_operation, plan_system, OperationReplay} and takedown denial key/codec helpers become crate-private. Use authenticated admin::Engine operations or the pipeline; no adapter or application called these helpers.
  • [embedder: breaking API] download becomes crate-private and unused takedown::ACTIVATED is removed. Bindings use the pipeline download API; activation is determined by the deployment's takedown configuration.
  • [embedder: breaking API] Purge and indexed slices now use one budget::SliceBudget, re-exported at both existing paths. Purge callers migrate charge(n) to charge_operations(n); request callers retain charge()/charge_many(n). Local purge reservations still remain charged after parent refusal.
  • [embedder: breaking API] The Cargo test-faults feature is renamed to internal __test-faults in the server, adapter and dependent test hosts. Update Cargo feature lists; the wire capability and conformance script's --test-faults flag are unchanged.
  • [embedder: breaking API] The 0.5.0 comparison also includes earlier unreleased API changes: indexed::publication::Exhaustion::IndexLookup is a new exhaustion variant (handle it in matches); removed pipeline::{published, PublishedSource} and Pipeline::with_published_source are replaced by verified stored publication reads (remove the published-view feature); removed PipelineConfig::inspection_mode is replaced by the synchronous Inspect hook (remove the field); removed PurgeDelivery::fire_with_local is replaced by constructing PurgeDelivery with the local cache and calling its timer handler. Fault-hook types and test methods remain available only with the renamed internal __test-faults feature; enable it explicitly in repository tests.
  • [embedder: breaking API] mkit_server::sql moves to mkit_server_worker::sql, and the sql feature is removed. Import SQL store types from the Workers adapter and remove sql from mkit-server feature lists.
  • [embedder: breaking API] mkit_server::telemetry::pressure and mkit_server::relay::WORKER_* move to mkit_server_worker::telemetry::pressure and mkit_server_worker::relay::WORKER_*. Update imports to the adapter.

mkit-server main now accumulates 0.6 changes; the server-semver check compares against the 0.5.0 baseline as the 0.6 release. Reset unsupported stored shapes instead of upgrading or downgrading them.

  • [embedder: breaking API] Request reservations and outcomes record their operation. ReservationV1::{Pending, Committed, Aborted, ReadServed} require StoredProcedure and are #[non_exhaustive]: use the constructors with the operation argument and match with ... ReservationV1::Expired, Ticketed, OutcomeRef and AbortReason are unchanged.
  • [embedder: breaking API] Outcome gains procedure: Option<Procedure> and visibility: Option<RepoVisibility>. Request outcomes identify their operation, including a reservation the crash reconciler abandons; expired tickets derive BeginUpload. System RepoStorageChanged outcomes omit the procedure. Visibility is present only for visibility-change outcomes. Migration: use Outcome::new or Outcome::from_reservation for construction, and read the optional operation/visibility fields when handling outcomes.
  • [embedder: breaking API] mkit_server::indexed::publication::{verify, verify_inspected} are removed. They had no callers: the pipeline verifies within the request's publication ledger.
  • [embedder: breaking API] mkit-transport-connect: ConnectTransport::repository() now returns Option<&RepositoryIdentity> (None for an authority-mode namespace, which names no owner). Migration: use remote_identity() for the addressed repository, or repository() where a self-certifying identity is required. No stored-format change.

Removed

  • Remove mkit-server-worker, apps/vcs-worker, and apps/embedded-worker, along with their deployment guides, runtime probes, and build and CI wiring. mkit-server remains the reference implementation with its existing features and wasm32 support. Native HTTP wire conformance runs through the in-process TestHost. Embedders can host the reference server through its library API.
  • [embedder: breaking API] The Workers-only published-view optimization (ref snapshots in R2, its timer, PublishedViewConfig, fetch_configured, ns_object_configured, the PUBLISHED_SNAPSHOTS binding) and the mkit-server published-view cargo feature with pipeline::published and Pipeline::with_published_source are removed. Nothing enabled them by default. timers::registry::kinds::PUBLISHED_VIEW is removed and timer kind 10 stays reserved. embedding_pipeline and purge::LocalCache no longer carry snapshot parameters.
  • [embedder: breaking API] The production embedder's deprecated profile alias is removed; use LAUNCH_PROFILE=paid-workers.
  • [embedder: breaking API] The never-set PipelineConfig::inspection_mode and WorkerConfig::inspection_mode fields and the no-op Worker inspection marker guard are removed. Enabling inspection still requires an empty store; this is now documented as an operator requirement.
  • [embedder: breaking API] [embedder: stored-format change] [embedder: store reset required] The namespace-scoped purge no longer walks the namespace's repository catalog; with no per-repository URL paths that step deleted nothing. A namespace purge deletes its own exact paths from the local cache and is delivered once to the configured sink, unchanged. PurgeDelivery::fire_with_local and the Worker NamespaceDelivery handler are removed. Only current checkpoint rows resume; reset unsupported in-flight state.

0.5.0 - 2026-10-02

The production mkit server arrives as a runtime-agnostic core (mkit-server) with a Cloudflare Workers adapter (mkit-server-worker, not published to crates.io) and a conformance harness (mkit-server-conformance, not published). Every mkit-* crate moves to 0.5.0 in lockstep. mkit-server is published to crates.io for the first time; mkit-cli depends on it.

Breaking changes

  • mkit serve --http and mkit serve --listen-enc, with their companion flags, are removed. mkit serve keeps the mkit+ssh:// session over stdio; each removed flag exits with a usage error.
  • mkit-transport-connect no longer exposes the server-side service (its server cargo feature is gone); only the client transport remains.
  • The standalone native mkit-server binary, the mkit-server-native crate and the mkit-server container image are removed. The release ships the mkit CLI archives only; the production deployment target is the Workers adapter.
  • The Workers launch profile selector LAUNCH_PROFILE value uno is renamed paid-workers; uno remains accepted as an alias.
  • Stores written by earlier unreleased server builds are not migrated; fresh stores are required for the new ListRepos visibility indexes and the takedown and inspection metadata.
  • (client) Remove ConnectTransport::with_atomic_advance for the upcoming 0.5.0 release. GetServerInfo is the sole source of atomic-advance capability (WP-1.16).
  • (client) Connect remote URLs whose path is not a repository identity now fail at open with MalformedUrl, even against servers that ignore the path: for example /org/repo, /MyRepo, percent-encoded paths, dot segments, and extra or missing slashes after the scheme. Use an empty path or a lowercase bare name (mkit+https://host/myproj) for single-repository servers, and <ed25519-… | 0x…>/<name> for namespaced ones (STC §7.4) (WP-1.16).
  • Breaking (server): D34 is now the default sharding for Connect deployments: the Worker's unset SHARDING means d34 (wrangler.jsonc sets it). There is no migration: a database written single, or written before sharding was configurable and holding data, answers 503 until SHARDING="single" is pinned. Under Single addressing the default write quota is now counted per (signer, branch). The client skips a listed branch that a stale ListRefs names after its delete (head and packmap both absent) instead of failing with PackmapMissing (WP-1.28c).
  • Windows is no longer a supported build, test or release target. mkit-keystore removes the backend-windows-credential feature and BackendKind::WindowsCredentialManager; "windows-credential" is now an unrecognized backend name. Run mkit under WSL. See Removed.
  • mkit-cli removes its http-transport cargo feature (with mkit serve --http).
  • mkit-core::merkle replaces its provisional inclusion-proof API with commonware-identical BMT proofs and id-based verifiers, and HistoryError::Mmr is renamed HistoryError::Mmb (ancestry roots and proof bytes change). See Changed.

Added

  • (spec) Indexed-mode server contract, D32 file extraction, repository-isolated resolution, and additive PendingVerification detail and max_delta_chain_depth discovery field (WP-4.4).
  • (spec) Add bounded admission credential headers to hooks.v1 AdmitRequest (WP-3.6b).
  • (docs) Add an unsupported TypeScript mppx admission Worker reference for mkit.server.hooks.v1, with delayed settlement and optional signed hooks (WP-3.14).
  • (core) Add pack::rewrite_excluding and pack::Rewritten for budgeted pack rewrites: excluded objects are dropped, deltas with excluded direct bases become raw, and unchanged packs retain their exact bytes.
  • (proto) Add the M1 discovery and ticketed upload RPCs, upload ticket fields, ref deletion fields, and ref-list paging fields to mkit.transport.v1 (WP-1.2). The server returns unimplemented ("not implemented yet") for these RPCs and non-default new request fields until their implementing WPs land. ListRefs.page_size is temporarily ignored; existing requests retain their behavior.
  • (spec) SPEC-SERVER v1 M3 pipeline/outcome guarantees and the signed mkit.server.hooks.v1 contract, proto, and golden vectors (WP-3.6).
  • (core) pack::DeltaBaseSource: the external delta-base lookup is now an explicit, generic parameter, so a server can resolve bases only from the pushing repository's membership (PRD §6.5, no existence oracles). &ObjectStore implements it (PackReader::read is unchanged, byte for byte and in error order); NoExternalBases is the self-contained-pack source. A source not marked VERIFIED has its bytes re-derived, and anything but the requested object is DeltaBaseMissing, the same error as an absent base. New store-less pack::decode_entries_with(pack, bases, limits, sink) validates and decodes a pack in pack order, handing each DecodedEntry to sink (DecodeReport summarizes). DecodeLimits::max_decoded_bytes (default 1 GiB, per call) caps what a decode may hold: every compressed entry's claimed size and every delta's declared result length are charged before anything is decompressed or applied, and every external base as it is fetched (credited back after its last use), so a tiny pack cannot pin gigabytes (PackError::PackfileTooLarge).
  • (core) verify::verify_push(tips, mode, source, known) / PushReport: incremental push verification before refs move. It walks every new tip's closure through the shared closure BFS, re-hashes each object, checks commit/remix/tag signatures, stops at a caller-supplied frontier of objects already verified in the repository, and reports missing, corrupt, badly signed objects and non-commit tips.
  • (core) sign::verify_object_signature(&Object): the per-type signature check (verify_commit / verify_remix / verify_tag; Ok for unsigned kinds). The CLI's fetch-side signature check now calls it.
  • (core) verify::build_disclosure_from: builds SPEC-DISCLOSURE bundles through any verifying store::ObjectSource (a per-repository index or the global object CAS), not just the on-disk ObjectStore. build_disclosure is now a thin wrapper; bundle bytes are unchanged (the disclosure golden regeneration is a zero diff).
  • (core) pack-ruzstd feature: a decode-only, pure-Rust zstd backend (ruzstd 0.9, with twox-hash for frame checksums) that lets a wasm32-unknown-unknown build read SPEC-PACKFILE v2 0x03/0x04 entries under the same bomb guards, length checks and one-frame rule as the C path. It does not pre-allocate the claimed size, but a frame that decodes to its claim peaks at about 3× the claim (C: about 1×), because ruzstd's ring buffer rounds up to a power of two and read_to_end grows the output by doubling; a 512 MiB claim measured about 1.55 GiB RSS. PackWriter still compresses only with pack-zstd, and pack-zstd decodes when both features are on. No consumer enables it yet. Also added: C-encoded v2 fixtures in rust/tests/golden/pack-v2/ (SPEC-PACKFILE §10 #20, including frames with 4- and 5-byte literals headers), a C-vs-Rust differential test suite, a wasm32 test lane (scripts/wasm-ruzstd-check.sh, crate mkit-core-wasm-check, in just ci-scripts), a pack-ruzstd nextest run in just ci, and a pack-ruzstd graph check in scripts/check-wasm-dep-graph.sh. Accepted deviation: the WP-4.1 brief called any frame that ruzstd accepts and C rejects "not tolerable". Such frames remain on malformed input and are accepted as a documented residual (no consumer yet; object ids are content-derived). Over 850k mutated frames: 134 accepted only by ruzstd, 2,580 accepted only by C, 4 accepted by both with different bytes, no panics. Consumer requirements are in docs/INVARIANTS.md.
  • (server) mkit-server crate (internal foundation for the production server, production server work): repo and namespace identifiers, principals, the typed Operation model, a transport-neutral ServerError with redaction and response shaping, the MaybeSend/Clock/Spawner/send_wrap runtime model and a Metrics facade. Builds for native and wasm32; first published with 0.5.
  • (server) mkit-server storage contract core: the key-level NamespaceStore (one declarative Batch of Absent/Present/Equals preconditions and a NotAfter deadline on the backend's own clock, plus puts and deletes; get/has/get_many/ordered scan), the key-layout registry, and the in-memory MemoryKv reference backend behind the memory feature. No SQL required of a backend.
  • (server) mkit-server replay-ledger model (a stored result can never be a challenge or a retryable outcome), versioned value codecs, typed readers over any NamespaceStore, and the content-addressed BlobStore / PackSink contract with the MemoryBlobStore reference backend.
  • (server) mkit-server-conformance crate (publish = false): the storage-backend conformance suite, the gate for third-party backends. 74 generic cases over NamespaceStore, BlobStore and ContentIndex (preconditions, scans, limits, capabilities, the NotAfter deadline, capacity, cancellation, crash/restart, export/import, golden encodings); a backend implements KvHarness, declares its expected skips (any other skip fails) and invokes storage_suite!. Contract changes: scan rejects a cursor outside the scanned range as Invalid (so ListRefs rejects a page token from another prefix instead of restarting); a blob body over 1 MiB MUST be streamed in pieces of at most 1 MiB (MAX_BLOB_PIECE_BYTES); rule 4 states what later reads of a dropped apply may see; an import into a RefsOnly store refuses an export whose layout version differs from the store's implicit one. SemVer: unreleased API.
  • (attest) SPEC-WRITE-GRANTS grant codec behind the new grants feature (not default): mkit_attest::grant parses and encodes the mkit-write-grant:v1 statement strictly (one canonical encoding, every §3.5 rejection, no repair), with the §3.1 text rules, ref scopes and §8.1 effective flags (never for packmap refs; packmap_head maps one to the head that covers it, §8.3), the grant id (only from parse_with_id or Grant::id, never from caller bytes), and the X-Write-Grant header (<statement>.<scheme>.<blob>, strict unpadded base64url). New fuzz target grant_parse. Golden and reject vectors under rust/tests/golden/grants, cross-checked by scripts/golden/grants_ref.py. SemVer: additive.
  • (attest) SPEC-WRITE-GRANTS verifier (feature grants): the mkit-write-epoch:v1 and mkit-repo-visibility:v1 statement codecs, the pure epoch_transition (§5.2 check 7, overflow-safe, with the retry rule), the ed25519 owner scheme (verify_strict over the BLAKE3 of the statement, bound to ed25519- namespaces; the ECDSA schemes return SchemeNotImplemented until WP-2.5), and the stateless verifier: verify_grant_owner (§7 steps 1, 3, 4, cacheable by header bytes) plus OwnerVerified::check (steps 2, 5–7, 9, 10), verify_epoch_statement and verify_visibility_statement (distinct VerifiedEpoch and VerifiedVisibility results, valid at one now and never to be cached), and verify_for_registration (§10). Expiry is exclusive (now < expiry), unlike auth v2. VerifierConfig holds the deployment's own audience and refuses a loopback one unless built with new_allowing_loopback (§3.2, §10), and refuses webauthn-p256 without a relying party (§4.3). The verifier results have no public fields or constructors; a VerifiedGrant's effective flags never cover packmap refs directly.
  • (attest) SPEC-WRITE-GRANTS ECDSA owner schemes (feature grants): secp256k1-eip191 (strict low-S recovery from the EIP-191 digest, v 27 or 28, and the recovered address must equal the 0x namespace) and webauthn-p256 (the four-field blob, low-S raw signature, curve-checked key whose address must equal the namespace, UP flag, relying-party id hash and that relying party's origin, and a strict clientDataJSON: duplicate member names rejected at any depth, nesting at most 64 deep, finite binary64 numbers only, type, exact challenge, crossOrigin, no topOrigin, signature over the received bytes; SPEC-WRITE-GRANTS §4.3 now states the depth and number limits). The legacy DSSE helper verify_webauthn_wrapping_with_policy is documented as lax and for self-checks only. mkit-attest now enables serde_json's float_roundtrip feature (correctly rounded number parsing, so a value that rounds to f64::MAX is finite); by Cargo feature unification this applies to every serde_json parse in a build that includes mkit-attest (the CLI, mkit-wasm, and more): number parsing changes only in the last bit in rare cases, where it becomes exact, and gets somewhat slower. New RelyingParty, WebAuthnAssertion and webauthn_challenge; OwnerVerified/VerifiedEpoch/ VerifiedVisibility::relying_party. Breaking (unreleased API): VerifierConfig::new and new_allowing_loopback take the relying parties (webauthn-p256 needs one; new refuses loopback ones), a cached OwnerVerified re-checks its relying party in check, and GrantError::SchemeNotImplemented is replaced by per-rule variants. Signed goldens and verify-reject vectors for both schemes, cross-checked by scripts/golden/grants_ref.py with python-ecdsa and pycryptodome. New fuzz target epoch_visibility_parse; signed goldens {grant,epoch,visibility}-ed25519.json and reject/verify-*.json, cross-checked with pycryptodome by scripts/golden/grants_ref.py. SemVer: additive.
  • (core) repo_identity: the SPEC-TRANSPORT-CONNECT §7.4 repository identity grammar (Namespace, RepositoryIdentity; lowercase only, at most 173 bytes; bare names only through parse_bare_allowed). SemVer: additive.
  • (server) mkit-server streaming pipeline: Pipeline::begin_upload returns an UploadSession (fresh, resume or replay of the signed operation; the replay record is reserved InFlight { resumable: true } with the quota charge before any chunk, and committed by a second batch planned with its own NotAfter deadline after the stream), and Pipeline::download returns a DownloadStream of 800 KiB chunks. Both hold at most one chunk. Resuming an in-flight upload is legacy M0 behavior for un-ticketed uploads (vcs-worker parity), a deliberate exception to SPEC-TRANSPORT-CONNECT §7.1 step 2 that goes when WP-1.9 ships replay-exempt ticketed uploads. A replay verifies the stream without writing the blob; a final pre_receive rejection is stored as the operation's result; an upload that outlives its envelope returns OK once the blob is committed, leaving its in-flight record to the pruner. The test-faults feature adds FaultHooks at five points and per-request TestDirectives. SemVer: unreleased API.
  • (server) mkit-server fs feature (native, std-only, no async runtime): FsBlobStore streams uploads into packs/<64-hex> and publishes only after BLAKE3 and length verify (temp file, fsync, rename, directory fsync); bodies over 1 MiB stream in 64 KiB pieces. FsLayoutStore keeps a repo's refs/ refs as FileTransport ref files under its ref lock (.mkit/refs/.lock), evaluating a NotAfter deadline under that lock. An undecodable ref file is Corrupt, a ref whose file clashes with another ref's directory (or the reverse) is Invalid, and a full disk or quota is Full. Both pass the storage conformance suite. SemVer: unreleased API. Behavior change: mkit serve wrote a ref named outside refs/ (e.g. main) to <root>/main; the FS store keeps such names in the server-side .mkit/server/rows/ store instead, invisible to the CLI and FileTransport, so a name like packs/<hex> can no longer overwrite a pack. (The pipeline now refuses such names outright; see the mkit-server refs/ entry above.)
  • (transport-file) FileTransport::with_ref_lock runs a closure under the ref lock with a LockedRefs handle (read_ref, update_ref, delete_ref, and atomic write_file/remove_file confined to .mkit/server/, symlink-escape guarded); typed RefFileError; strict read_ref_strict/list_refs_strict (an undecodable ref file is an error, not absent); server_path, root, temp_path, sync_dir and create_dir_all_durably are public. Ref writes now fsync the parent of every directory they create, and a ref whose file clashes with another ref's directory (or the reverse) is InvalidRef, not a RemoteError. A panic under the ref lock no longer poisons the transport: the next writer recovers the in-process lock (it guards no data). SemVer: additive.
  • (server) SQLite metadata backend: mkit-server's sql feature adds SqlKvStore, the NamespaceStore contract implemented once over a synchronous SqlConn (one kv table keyed by partition and key, each batch one transaction, NotAfter read on the backend clock inside it) with versioned, forward-only schema migrations that refuse a newer schema. An optional Capacity sets a hard cap and a soft limit a reserve below it: batches with a put return StoreError::Full at the soft limit, and the reserve keeps delete-only batches (which can split b-tree pages) from ever hitting the engine limit. It compiles for wasm32, so Durable Object SQLite can share it. Not in the mkit-cli graph. SemVer: unreleased API.
  • (server) New mkit-server-worker crate (publish = false), the storage half of the Cloudflare Workers adapter: R2BlobStore streams both ways (a put spawned at begin and fed through a depth-1 channel; the blob's last byte is withheld until its BLAKE3 verifies, so only verified bytes are ever published, and an abort or a dropped sink leaves nothing; bodies read back in pieces of at most 1 MiB; a 64 MiB per-blob cap as an M1 stopgap), DoSqlConn (Durable Object SQLite under the shared SqlKvStore, batches in transactionSync, a soft cap below the 10 GB limit measured by databaseSize, which excludes free pages), the Durable Object request handler, and DoNamespaceStore, which routes each partition to its own Durable Object (M0: REFSTORE/"root"). It passes the conformance suite with zero skips over simulated R2 and Durable Object backends. Adding workers-rs 0.8.6 moves rust/Cargo.lock from wasm-bindgen 0.2.127 to 0.2.128 (with js-sys/web-sys 0.3.105, wasm-bindgen-futures 0.4.78), which mkit-wasm shares. SemVer: unreleased API.
  • (transport-s3) sigv4::sign_request_with_payload_hash signs a request for a body whose SHA-256 the caller already has, so a streamed upload need not hold its body in memory; sign_request now delegates to it, byte for byte. SemVer: additive.
  • (transport-enc) serve_tcp_listener: the async accept loop on a caller-bound TcpListener, for a server already on a tokio runtime, with separate caps on handshakes and sessions (ListenerLimits) and a shutdown future; it retries a transient accept error (a reset connection, descriptor exhaustion) and, after shutdown, stops accepting and waits for the sessions in flight (the caller bounds the wait). The blocking serve_tcp_* entry points share its accept loop with their signatures and behavior unchanged (uncapped, stopping at the first accept error and leaving sessions running). SemVer: additive.
  • (core) Resumable-part building blocks (SPEC-TRANSPORT-CONNECT §7.6): write_auth::ContentCommitment parses and formats body:, pack: and the new part:<ticket>:<index>:<subtree>:<len> commitment, and verify_headers_with(.., ExpectedCommitment::PartStream, ..) verifies an UploadPart envelope; the new upload_parts module validates part geometry, streams a part into its BLAKE3 subtree chaining value and merges part values into the pack id. Golden vectors under rust/tests/golden/{uploads,auth-v2}. SemVer: additive.
  • (attest) mkit_attest::eth, behind the new default-off grants feature: Keccak-256 (original Keccak, not SHA3-256), the EIP-191 personal-message digest, strict low-S secp256k1 recovery from r ‖ s ‖ v, curve-validated secp256k1 and P-256 address derivation, and the SPEC-WRITE-GRANTS §4.4 client normalizers (EIP-191 v/high-s, P-256 DER to raw low-S r ‖ s), with a module-local EthError. Adds the sha3 0.11 dependency (optional). Golden vectors in rust/tests/golden/grants/eth-primitives.json, cross-checked against Foundry cast, viem and pycryptodome.
  • (core) Closure verification now has a pull-based ObjectSource/verify_closure_streaming API and a native verify_closure_store helper. ClosureReport gains the additive unreferenced_checked field: map and pack verification set it to true, while streaming verification leaves unreferenced empty and sets it to false. SemVer: additive in this pre-release, but the new report field is a struct-literal break for external constructors.
  • (cli) Local mkit closure verify now reads only reachable objects by default; --show-unreferenced retains the full-store enumeration needed to compute that list. JSON reports whether the list was checked, and text mode explains how to request it when it was not.
  • (wasm) Closure pack and manifest verification now expose unreferenced_checked in their JSON results and use borrowed raw pack entries during verification. SemVer: additive.
  • (wasm) verify_disclosure JSON includes step_inner_roots (hex array) and chunk_inner_root (hex or null). SemVer: additive.
  • (cli) mkit verify-proof --format=json includes the same two keys. SemVer: additive.
  • Redesigned the workspace around Files, Changes, and History with grouped version saves, attributed diffs, explicit conflict recovery, session-scoped drafts, and progressive terminal and project controls.
  • Hosted public workspaces at /create: passkey-authorized demo remixes, a file editor, a real terminal, and a server-hosted nanocodex agent that saves signed versions and continues when the browser closes.
  • WASM exports object_id, tree_decode, blob_decode, and remix_verify expose canonical object IDs, tree entries, binary file contents, and remix signature verification for browser and edge workspaces. These readers bound input size and reject invalid encodings; tree IDs retain their native Merkle semantics. No on-disk or wire format changes.
  • (core) New mkit_core::verify module (issue #1015 verifier kit PR 2) and docs/specs/SPEC-DISCLOSURE.md (draft-normative): partial-disclosure bundles that prove a single path, chunk, or byte range belongs to a commit id, with proof bytes on the order of a few KiB regardless of repository size. verify_object_id, verify_path, verify_chunk_with_meta, verify_blob_slice, and verify_blob_len_proof are the small, independently usable primitives (each corresponds to one hop of commit → tree steps → leaf → chunk/slice); verify_disclosure decodes a self-contained wire bundle (magic "MKDP", commonware-codec conventions: fixed integers big-endian, every length an LEB128 varint) and composes them; the native-only build_disclosure (needs ObjectStore) is the producer side, taking a Selector::{Object, Chunk, Range}. Every check is stated against an object id — never a bare BMT inner root or an unauthenticated path/offset claim (see docs/INVARIANTS.md). A ChunkedBlob chunk's total_size/chunk_size is authenticated alongside its content id in one multi-proof over BMT positions {0, index + 1} (merkle::verify_chunk_with_meta_leaf, a new pub(crate) helper — the verifier computes the metadata leaf itself, never accepting one as caller-supplied input, preserving verify_chunk's existing "reject position 0" rule). Byte ranges are proven with Bao (BLAKE3 verified streaming) over each object's canonical bytes, so the Bao root equals the object's own id; a ChunkedBlob range additionally supports proving the disclosed range's absolute file offset via a complete 0..index set of per-preceding-chunk length proofs. New dependency: bao = "0.13" (pure Rust, wasm32-clean — already proven by mkit-wasm's existing use of the same crate). Golden vectors: rust/tests/golden/disclosure/ (10 accept, 12 reject vectors), consumed by rust/crates/mkit-core/tests/golden_disclosure.rs. New fuzz targets disclosure_decode and verify_disclosure (docs/FUZZ.md, .github/workflows/fuzz.yml). Scope is partial disclosure only; closure/full-disclosure export is a separate profile (issue #1015 PR 3) and non-membership proofs are reserved as payload_kind 3. SemVer: additive — a new module and a new optional dependency; no existing public API, object id, signing byte, or wire format changed.
  • (core) Closure profile (issue #1015 verifier kit PR 3, SPEC-DISCLOSURE §7): full disclosure against a commit id. children / ClosureMode {Snapshot, History} is the single source of truth for object references; store-backed reachable_snapshot joins reachable_objects (history). PackWriter::new_raw_only never compresses and rejects deltas, always finishing as v1 with only 0x00 entries. PackEntries is a store-less pack iterator (PackReader::read consumes it; existing pack goldens and the pack fuzz target are unchanged). verify_closure / verify_closure_packs / verify_closure_manifest / export_closure are wasm-safe except the native export_closure. The "MKCL" v1 manifest is a convenience index of pack hashes; the root id is the only trust anchor. Golden vectors: rust/tests/golden/closure/. New fuzz targets pack_entries and verify_closure. SemVer: additive — new APIs; PackReader behavior unchanged.
  • (cli) mkit prove, mkit verify-proof, and mkit closure export|verify (issue #1015 verifier kit PR 5): CLI for partial disclosure (a path, chunk, or byte range against a commit id) and full-disclosure closure packs. Hand-maintained surfaces updated (docs/CLI.md, man/mkit.1, completions, MCP tools mkit_prove / mkit_verify_proof / mkit_closure_verify). Distinct from mkit git export. SemVer: additive.
  • (wasm) Disclosure and closure verification exports (issue #1015 verifier kit PR 4): verify_disclosure / disclosure_payload_bytes, verify_closure_packs / verify_closure_manifest, verify_tree_entry / verify_chunk, chunked_blob_decode, blob_bao_encode / blob_bao_slice / blob_bao_verify_slice (canonical blob bytes; existing raw bao_* exports are unchanged for the web streaming demo), and wrap_object_id. Structured results are JSON; hashes are 64-char lowercase hex. Objects stay at the 16 MiB workspace cap; bundles and concatenated closure packs are capped at verify::MAX_BUNDLE_BYTES (64 MiB). Native tests replay the proofs/, disclosure/, and closure/ golden vectors through these exports. SemVer: additive.
  • (docs) docs/VERIFY.md (issue #1015 verifier kit PR 6, closing the series): the user-facing guide to verifying an mkit commit hash for someone building a verifier or a DA provider — the byte-level authentication chain from commit id to leaf, the three-part trust model (content↔commit id, commit↔signer, signer↔identity), worked CLI/Rust/TypeScript examples for both full disclosure (closure profile) and partial disclosure (bundle), commonware-BMT interop notes, a from-scratch (no-Rust) conformance checklist against the golden-vector corpus, and a reference table of every CLI command, wasm export, and core function in the kit. Every command and code snippet on the page was run (CLI against a real repository; Rust via a compiled scratch check; TypeScript kept to the documented mkit-wasm API) while writing it. Linked from README.md, SKILL.md, and docs/ARCHITECTURE.md (already reachable through the docs MCP's existing get_file/search_docs tools). SemVer: none, docs only.
  • (wasm) MAX_CLOSURE_INPUT_BYTES (1 GiB, matching mkit_core::store::MAX_RAW_OBJECT_SIZE): verify_closure_packs and verify_closure_manifest now cap their concatenated packs input independently of verify::MAX_BUNDLE_BYTES (64 MiB), instead of reusing the disclosure-bundle cap for both shapes. A closure is every object reachable from a commit — a realistically much larger input than a handful of disclosure proofs — so it gets its own, separately tunable ceiling. SemVer: additive; strictly widens what a wasm closure verifier accepts (a pack set between the old 64 MiB cap and the new 1 GiB one, previously rejected on size alone, is now admitted and verified normally).

Changed

  • Server: add ListRepos, with bounded namespace-name pagination and authenticated continuation tokens. Atomic coordinator visibility indexes hide private names from public listings; owners and approved namespace authorities list all repos. Grants retain public listing rights only. Fresh stores are required.
  • HTTP ref-path files: serve MP4, WebM, MP3, Ogg, WAV, HEIC, Markdown and CSV inline with their media types, preserving encoded filenames, security headers and byte ranges. Dangerous extensions remain binary attachments.
  • HTTP object readback accepts streamed chunk-offset sidecar tails from R2.
  • Indexed verification, extraction and preservation count canonical Blob framing around the 1 MiB payload boundary while retaining the 48 MiB slice allowance.
  • Server conformance: add a debug-only memory-backed in-process HTTP host for contract tests, with origin-bound auth-v2, signed loopback hook support, and an explicitly advanced manual clock.
  • Server: add default-off durable inspection mode, a guarded repository flag registry, and bounded per-advance hold records as storage foundations for post-launch asynchronous inspection. Content holds are repository-wide under Single and D34; released manifests fence delayed writes.
  • Takedown-on publication and reader performance: sixteen-shard authoritative descriptor directory, resumable timer-12 pair verification before ref acceptance, caller canonical byte limits and typed object metadata. Deprecated mixed object_sizes remains available. Unshipped metadata codecs require fresh stores.
  • Worker embedding: supplied Admission, Authorizer/Authority and OutcomeSink capabilities satisfy launch validation without an unused external hook channel. fetch_with now takes explicit HookCapabilities; shared config factories use from_env_with_hooks/from_vars_with_hooks (delta review 8b-1).
  • Launch builds forward the Worker pure-Rust decoder to the server feature; mandatory PR gates run the isolated 48 MiB allocator regression (4-1, 12-3).
  • Object reader and URL batches retain the expiry of cached reachability proofs. Only fresh published walks renew them after rewind/deletion (6-1).
  • Server: retain retryable Takedown acceptance with its stable id until every denial is active. Exact retries resume bounded activation; timer-15 recovery finalizes the original nonce and operation result (delta review 7-1).
  • Worker preservation acquisition: bound scheduled reconstruction to 48 MiB by evicting previous delta intermediates and retaining only the current base. Preserve 50-hop, 1 MiB canonical chains, existing frame admission, corruption checkpoints and the 700-call slice budget; inline profiles are unchanged.
  • Server (WP-4.18, phase 1, R-194): explicit Paid indexed Workers launch selection and startup validation for optional HTTP/URL tokens, signed hooks/binding, synchronous inspection and private scanner retrieval. Worker proofs stay unsupported. Leases/GC remain off with permanent retention. Complete preservation configuration activates its core and restricted admin catalog; the launch build enables R-203’s bounded pure-Rust zstd decoder. Worker HTTP mounts retain read settlement in the fetch context; ticket and implicit packmap checks use at most six simultaneous backend responses. Added local conformance/evidence skeleton. Preservation and the restricted catalog are integrated; deployed CPU and whole-isolate memory evidence remain operator acceptance work.
  • Worker embedding (supported, 0.x): combine programmatic configuration, published snapshots, custom Outcome and purge sinks in NsObjectBuilder; generate the five DO classes with durable_objects!. Host-routed admin dispatch retains ADMIN_KEYS authentication, and programmatic ref policies validate before serving. Add a service-binding streamed-upload example and its wasm check. These APIs are unpublished, consumed at a pinned git tag.
  • Connect client transport: retry unary reads and replay-safe auth-v2 writes once on a fresh connection when a reused HTTP/1 socket fails before any response bytes; preserve the signed envelope and deadline. Streaming RPCs start on fresh connections and are never replayed.
  • Server: align the receipt-golden test SHA-256 dependency with the repository sha2 0.11 crypto channel (launch review 12-2).
  • Server: a purge that creates positive shared backlog also schedules the existing outcome-delivery wake, so later paid read outcomes survive purge completion, reconciliation and restart (launch review 3-1). Zero backlog retains wake ownership until the guarded kind-8 drain, preventing duplicate wakes across repeated purge cycles; content shards register the same driver.
  • Server: completed identical Takedown retries replay their stored response after role changes and without runtime operations. ReadPreserved retains fresh role and retention checks on every retry (launch review 7b-1).
  • Server: automatically enqueue audited cache purges with takedown acceptance, denial activation and late-holder ownership. Checkpoint newly discovered repository purges with timer-15 progress and immediately invalidate configured local caches (launch review 7a-2, existing R-190). Audit source snapshots and immediate invalidation share the enclosing request or slice call allowance.
  • Scheduled indexed verification reserves R-203 decoder scratch within the existing 48 MiB slice allowance, preserving window geometry and frame admission. Delta slices release the idle reader before source reconstruction; allocator regressions cover corruption, nested decode and valid custom limits.
  • Server: cap aggregate object-index candidate retention at a charged 4 MiB, bound membership joins and Worker request allocation, and enforce the current indexed pack-byte limit before Scheduled job claims/reuse and timer decoding. Verified reuse returns the exact advertised pack-size error. Scanner retrieval supports ticketless ref writes with an empty Inspect batch and a capability granting no access to previously published packs.
  • Server: keep relay progress continuations after the current wake when retry metadata retains an older due time. Update timer, verification-job and published-view tests to assert retained capped retries and recovery; recognize the HTTP content-header golden as a JSON table in the wasm verifier, and correct the route property's reserved-name fixture expectations.
  • CLI (mkit-cli, additive): add http.sslCAInfo and overriding MKIT_SSL_CA_FILE for extra PEM trust certificates on all native Connect HTTPS remote RPCs and pack streams only. Keep Mozilla roots and chain/hostname verification; invalid CA files fail closed. The self-updater keeps OS trust and release checksums when present, ignoring both settings. S3 remotes are not yet covered. Browser trust is unchanged.
  • Server (R-205): DEFAULT_REPO_VISIBILITY=public|private on Workers selects visibility for repositories without a stored setting. The default remains public; explicit visibility wins. Set it at deployment creation: changing it affects all unset repositories.
  • Server (R-204): embedders can issue batches of up to 16 URL tokens through ObjectReader::issue_urls, sharing RPC authorization, epochs and minting, with bounded published-view reachability and denial preflight.
  • Server (WP-5.6a-3, R-190): add signed moderation GetTakedown/ListTakedowns, atomic audited SetLegalHold and freshly verified ReadPreserved streaming. Replay stores byte-free read descriptors; each attempt rechecks authority and retention. Add separate v1 status fields for acquisition, verification, discovery, legal hold and purged copies. Activation remains off until 4.18.
  • Core/server (WP-4.16c, R-202): in-process canonical object prefetch for embedders, including ChunkedBlob manifests and indexed content sizes; verified public/owner views share bounded reachability and global-denial proofs. Add wasm-clean store::MemorySource for synchronous disclosures and diffs, with native and Worker embedding entry points. No wire change.
  • Bound pure-Rust zstd block expansion before allocation, with a fixed 8 MiB window cap, and checkpoint corrupt preservation sources as terminal audited failures. Preflight compressed delta result headers against verified metadata before decode budgeting; genuine resource failures remain retryable. Worker admission and native C decoding are unchanged (R-203).
  • Server/Worker: bound raw timer-alarm enumeration and share tick limits across logical heads. Persist capped exponential backoff in the existing timer row for cold fairness, preserving payloads and original handler due times. The unshipped timer key codec gains retry metadata; timer payloads reserve enough headroom for a guarded move within the existing batch limit.
  • Server/Worker: keep immediate alarm continuations strictly after the current clock so bounded ticket, outcome and relay cleanup cannot strand an alarm chain. Test timer directives share tick exclusion with the autonomous driver, making due-timer and redelivery conformance deterministic.
  • Server/Worker (WP-4.16b, R-201): ordinary ref-path file responses now select Content-Type from a fixed extension allowlist and include inline/attachment filenames with RFC 5987 encoding and a sanitized ASCII fallback. HEAD and ranges share the headers; SVG/HTML remain binary attachments. Security, object-id routes, proofs, caching and private-token rules are unchanged.
  • Server: publication timer 12 resumes bounded dependency checks across alarms and restarts after guarded obligation, dependency or generation changes. Valid large D34 packmaps no longer stall at the whole-alarm call limit. Its internal timer-value codec changes; reset unsupported pre-launch stores.
  • Docs (WP-5.6a-2/3, R-190): specify finite root sweeps and support Any denial/ preservation with incomplete discovery pending the post-launch catalog. Distinguish verified preservation from unresolved completion; require byte-free admin replay and fresh audited verified streams. §14.7 signing/publication and full-profile requirements remain. PR2 owns preservation/retention/hold core; PR3 owns the restricted admin catalog. Activation stays off; no completed gates claimed.
  • Server/Worker (default-off, WP-4.10b-2, R-186): complete consumed-group extraction with closure before effects, union counts, bounded job header/body guards, resumable delta reconstruction and root-checked multipart uploads. Hold renewal and durable holder delivery precede Verified; release activation waits for 4.18.
  • Server (R-193): default-off private raw-pack retrieval for synchronous scanners, with separate capability MAC and scanner signing keys, bounded ranges, current-ticket lifetime checks, uniform not_found and global block enforcement on the Worker. Inspect remains metadata-only; retries keep their inspection id and mint fresh short-lived capabilities. Production activation remains gated by WP-4.18.
  • Server (WP-5.6a-1, R-190): independent V2 denial actions, verified pack inventory and signed, audited pending takedown intents; manual PurgeCache returns a purge id asynchronously and audits completion. Preservation and production takedown activation await WP-5.6a-2/3. Timer 15 is TAKEDOWN_WORK; no new key tag or migration.
  • Server (WP-5.5a, R-200): synchronous added-pack file inspection before apply, fail-closed retries without replay, signed remote Inspect, and advertised launch input limits (default 10,000 objects, maximum four inspectors). Inspect every Blob and ChunkedBlob, surplus included, without role reads; enabling inspection requires an empty store. Full classification follows in WP-5.5c.
  • Server/Worker (inert, WP-4.10b-1, R-186): pending-holder protection and atomic content-holder relay delivery retain protection beyond hold expiry; late blocked holders leave durable takedown requests for WP-5.6a. Add bounded verification projections, opaque ETag capture and default-compatible internal R2 upload callbacks. Worker Extract remains fail-closed; the driver is PR2.
  • WP-5.10: durable timer-11 automatic cache purge intents, signed global delivery with stable ids and fresh nonces, local invalidation and snapshot refill fences.
  • WP-5.11a: default-off signed admin framework with separate role keys, durable replay and gapless audit export. Automatic actions join the audit through the existing outbox relay; manual PurgeCache moves to WP-5.6a.
  • Server (WP-5.4, R-198): remove compatibility with unreleased publication stores; reset existing stores. Hold authority belongs to post-launch WP-5.5c; timer 15 and pv are freed.
  • WP-5.4: add persistent paired published refs, versioned membership and durable cross-ref dependency rechecks; route reader RPCs, snapshots, tokens and HTTP bytes/proofs through published values while retaining writer visibility and serving stops. Specify indexed permanent-retention launch without storage leases or GC, and publication Events: Committed means Sent; Delivered requires the contiguous published prefix.
  • Docs: add the early Paid Workers launch-readiness skeleton (WP-1.20, R-195 / R-198): D35 environment/key roles, operator reset/rollback/rotation/drills, empty user-owned evidence slots and a DRAFT user-only REL-1 prompt. Final readiness waits for 4.18; no staging, measurements or release actions are claimed.
  • Add optional namespace authority-generation fencing with dedicated deployment-authority statements, Worker configuration, independent D34 lease barriers and generation-bound upload tickets (WP-2.16). Ticket streams coalesce legal client frames into bounded 256 KiB storage checkpoints, preserving revocation checks within Worker request budgets.
  • WP-3.9c: default-off signed HTTPS Worker hooks with canonical origin signing, manual redirects, streamed response limits and aborting call timeouts.
  • Worker (inert): extracted-object storage can use root-pinned backend R2 multipart sessions with verified streamed parts and bounded finalization. Internal object receipts preserve opaque backend ETags without changing pack wire receipts; extraction and production activation remain separate work (WP-4.10b-multipart, R-192).
  • Server: kind-8 completion uses the guarded fresh backlog, preserving delivery after a concurrent outcome append (WP-3.13; correction to #1219).
  • Server conformance: M3 cases cover admission, CORS, CAS-loss and ticket expiry; the orchestrator rulings preserve pipeline ordering and accept ordered combined WWW-Authenticate lists on Workers (WP-3.13).
  • RPC: public mkit-rpc::hooks message types with JSON support and runtime-free HookSigner and HookVerifier, enabled by the hooks feature. The server re-exports its authentication surface for external hook implementers (WP-3.7b).
  • Server (Stage 2, inert): default-off http-objects adapter features mount raw escaped HTTP object routes with streaming Range/HEAD, read CORS on every response, query-free diagnostics, and public URL-token key documents. Worker token secrets use dedicated keys; retained keys also cannot repeat hook or enc keys. Public ref redirects are explicitly opt-in and disabled with admission (WP-4.16, R-179).
  • Server (Stage 2, inert): URL tokens now authorize private HTTP object and ref-path reads through staged signature, stateless binding and epoch checks, while preserving anonymous published access and Authorizer checks. Private immutable cache lifetimes are bounded by token expiry; private ref paths require revalidation. Active and retained token keys are separated from ticket secrets without exposing seeds (WP-4.15, R-178).
  • Server (Stage 2, inert): programmatic HttpObjectsConfig::admit_reads enables paid GET and HEAD reads, canonical JSON 402s, private admitted responses, durable pending reads before transmission, deadline enforcement and retained asynchronous byte accounting (WP-4.13, R-177). The shared credential selector now denies comma-joined selected payment headers, correcting #1212; non-Payment Authorization remains excluded. API: HttpReadRuntime injects retained tasks and deadlines, and HttpObjectRequest::header_names preserves credential name spelling.
  • WP-1.19: add an inert Stage 2 staging template and activation/backup runbook; provisioning, routes and CPU sizing wait until after REL-1.
  • WP-1.21 (Stage 2): default-off Worker published ref-index snapshots with bounded binary envelopes, atomic debounce/generations, private R2/Cache serving after coordinator authorization, signed bypass, inspection refusal and live fallback; existing Stage 1 entrypoints stay inert.
  • Server: built-in ref policy (SPEC-SERVER §9.7, programmatic, Stage 2): PipelineConfig::ref_policy with per-ref allowed operation signers (both modes) and fast-forward-only rules (indexed mode). In indexed mode a u-only write grant now fast-forwards, proven against this repository's history (IndexedConfig::max_ancestry_commits, default 256), and a ticketless head must be a member commit, remix or tag. API: verify_ticketed returns StagedCommits; VerifiedAuth::created_at_ms and IndexedConfig::max_ancestry_commits are new (WP-4.17).
  • Server: authorization, admission and outcome delivery can run in a remote hook service over signed HTTPS. Plain HTTP is loopback-only, redirects are never followed, and a remote admission replaces the default abuse quota. New in mkit-server: pipeline::Choice, hooks::HookVerifier (the hook service's side of the signature), HookSigner::public_key, TicketKeys::contains_secret.
  • Server: the Workers adapter can call a hook Worker over an unsigned ADMISSION_HOOK service binding (HOOK_ROLES, HOOK_TIMEOUT_MS, AUTHORIZER_ROLE), and adapter::fetch_with and ns_object_with take a deployment's own HookSet and outcome sink (WP-3.9). The Queue outcome sink and a signed Worker webhook are deferred (WP-3.9b).
  • Server (Stage 2, inert): mkit-server gains the default-off http-objects feature: Pipeline::serve_http_object serves repository objects and ref paths per SPEC-HTTP-OBJECTS (the URL grammar, published resolution, a bounded reachability proof for id URLs, ordinary Range and conditional requests, security headers and one uniform 404), reading extracted objects that this repository holds by range and everything else from its own pack entries. It needs PipelineConfig::http_objects, which requires indexed mode, and no adapter enables the feature or mounts a route (WP-4.12, R-169). SPEC-HTTP-OBJECTS §4 now says the global content store decides no membership or reachability but may supply the bytes of an id this repository holds. API: new Procedure::{HttpGetObject, HttpGetRefPath} and OpKind::HttpGet. Request queries use RedactedQuery, exposed only to the parser; ref scans bound all rows and pages, and ref paths peel up to 16 tags.
  • Server: the Worker registers the kind-5 namespace quota rollup on its ref shard, coordinator and root classes; the quota conformance cases run under D34 (per-branch) and a Multi + D34 case checks the namespace cap across branches after a forced rollup (WP-1.26b).
  • Server: the M1 exit wire conformance suite (WP-1.27): isolation replay, target-ref ticket caps and expiry, the D34 lag windows and D36 hinted reads over a held relay, idle and expired epoch-lease renewal, 64-ref writes, an over-32 MiB listing and bounded ticket growth; the test-faults timer directive also expires tickets and the Worker stats hook is scoped to one ref's shard under D34. mkit-server-conformance Profile gains ticket_per_ref and merge_paging_refs (WP-1.27).
  • Server: the Worker gains the GRANT_SCHEMES and WEBAUTHN_RPS vars and a test-faults-only UNSAFE_LOOPBACK_GRANTS, to configure write grants on Multi + auth v2 deployments. Any bad or partial value refuses to start; unset keeps grants off (WP-1.30b).
  • Server: a Worker can verify a ticketed pack asynchronously in checkpointed, budgeted alarm slices (timer kind 7, IndexedConfig::verification = Scheduled) with the same answers as native inline verification: the advance answers PendingVerification until the slices finish (Paid plan only; index rows are relayed after the decode reaches Done, and Verified waits for their delivery). The job lives in the new vc key class; kind 2 now also removes an expired unconsumed pack's verification state. Indexed mode stays refused on Workers until WP-4.10b (WP-4.8). mkit-core gains WindowReader::last_frame and pack::decode_entry_with (additive).
  • Server: in indexed mode, extract every ChunkedBlob (as its reassembled content, with a chunk-offset sidecar) and every file Blob of at least 64 KiB into the deployment-wide object store under its object id, before a pack is marked verified. Object keys are verified against a content root by a new PackSink::commit_with_root; holder rows carry a sequence and the consuming ticket; a hold protects each object until its holder is recorded. New IndexedConfig::{extract_min_bytes, max_extract_bytes}. Workers still refuse indexed mode (WP-4.10). API: IndexedConfig::max_extract_bytes is now Option<u64> (default 4 * max_pack_bytes); verify_ticketed takes the consuming ticket ids and needs a MultipartBlobStore; ContentIndex gains extend_hold and add_holder_unless_blocked, and release_hold is deadline-guarded; is_reserved_pack_keyspace is exported.
  • CLI: a Connect push that needs more than six data packs per advance is now split automatically along the branch's first-parent history instead of failing with PushTooLarge after uploading six packs. Every intermediate commit is a published state; the tracking ref follows each advance, so a re-run resumes, and later advances always compare-and-swap on the previous one. For a split push, stored write grants are checked for every advance before anything is uploaded, and an unsplittable oversize commit or merge is refused before any upload. --format=json reports steps (WP-1.17b).
  • Server: the Workers adapter takes an outcome sink with the same per-call timeout, budgets kind 8 by plan (Free: 8 sink calls per alarm), allows and exposes the payment headers for browsers and keeps repeated WWW-Authenticate fields (WP-3.5).
  • Server: mkit serve --root <DIR> serves the repositories under <DIR> addressed by <NAMESPACE>/<NAME> (from the path argument or a strict SSH_ORIGINAL_COMMAND), one repository per process, with writes restricted to the namespace's Ed25519 owner asserted by --principal <hex>. Transport-identity sessions grant pack membership implicitly for packs uploaded and verified in the same session — at most seven pending packs, consumed by the session's packmap write — so mkit+ssh:// pushes need no upload tickets. Denied writes answer INVALID_REQUEST "write not permitted" (WP-1.15).
  • Server: the Worker adapter can serve multi-repository deployments (the ADDRESSING/NAMESPACE_POLICY/NAMESPACE_ALLOWLIST/ UNSAFE_OPEN_NAMESPACES vars). Multi requires auth v2 with upload ticket keys — a deployment without them now refuses to start.
  • Server: add validated two-phase admission with bounded HTTP 402 challenges, redacted payment credential forwarding and committed-success receipt headers (WP-3.2).
  • Server: durably arbitrate admitted reservations with pending and terminal outcomes, reconcile abandoned reservations, deliver through a retrying outcome sink and apply per-shard outbox backpressure (WP-3.3).
  • Server (WP-3.7): add the default-off remote-hooks feature to mkit-server: Authorize, Admit and Outcome over mkit.server.hooks.v1 on a transport-agnostic HookChannel, with Ed25519-signed requests, fail-closed mapping and response bounds. Adds the Sleep timeout seam, and scripts/regen-hooks-proto.sh for the vendored codegen.
  • CLI: add mkit grant create|add|list and a user grant store under $XDG_CONFIG_HOME/mkit/grants/ (never repository-scoped). Owners sign with the mkit key, a software-keystore secp256k1 key, or an imported wallet or WebAuthn signature; the Connect client now presents the best stored grant, ranking the higher epoch first. New user-only config key grant.webauthn_rp. Issue grants for pushes that move a branch as cuf, not cu: while servers are opaque, updating an existing ref needs the f flag. mkit-keystore gains a defaulted KeySigner::sign_prehash_recoverable_secp256k1 for the software backends (WP-2.13).
  • CLI: add mkit epoch show|bump, mkit grant revoke [--prune] and mkit visibility set, waiting out the server's Retry-After. Connect client: new ConnectTransport::{get_grant_epoch, set_grant_epoch, set_repo_visibility} returning Completion, and a per-request signed-or-unsigned classification of SetRepoVisibility (WP-2.14).
  • Core and wasm: verify MKDS v1 multi-chunk disclosure spans against a trusted commit and build boundary-aware MKDP/MKDS range proofs (WP-4.14a).
  • Server (WP-4.6): batch repository object-index range reads on Workers, add bounded relay enqueue and delivery checks, increase paid relay throughput, and report index pressure, lag, backlog, and lookup caps.
  • Core: expose decoded pack frame metadata, single-frame decoding, and an additive resumable decode cursor for repository-scoped external bases.
  • Server Worker: stream verified multipart parts into CV-keyed R2 objects and verify the complete pack before publishing it; raise the ticketed pack cap to 1 GiB while retaining the 64 MiB single-upload limit (WP-1.12).
  • Server: expire unconsumed upload tickets with one guarded Expired outcome and best-effort upload-session cleanup (WP-1.14).
  • Server: maintain D34's 16-bucket ref-name index through relay upserts and deletes, and serve eventual paged ListRefs from it (WP-1.28b).
  • Connect push: BeginUpload tickets now follow each pack into AdvanceRefs, with bounded membership polling, nonce renewal, a six-data-pack advance limit, and a one-time re-plan for ticket, packlist, or delta-base failures (WP-1.17).
  • Connect push: large ticketed packs stream resumable parts with locally saved receipts, progress and an interruption hint; non-multipart deployments now advertise a compatible pack limit (WP-1.18).
  • Server conformance: restore Single and D34 wire cases for secp256k1-eip191 and webauthn-p256 owner grants (WP-2.6b).
  • Server: enforce grant ref scopes per change, including apply-time presence guards for ANY and head/packmap pairing (WP-2.7).
  • Server: add unsigned grant-epoch RPCs with owner statements, bounded epoch transitions and completion after leased shards are fenced (WP-2.8).
  • Server: verify auth v2 on every signed read, including the framed DownloadPack request, and add private repositories: SetRepoVisibility in envelope and owner-statement modes, a coordinator rv row, and not_found for every unauthorized private read, indistinguishable from a missing repository (WP-2.9). A read that carries auth headers but fails verification is now unauthenticated instead of anonymous, and a grant header on an unsigned Multi request is unauthenticated on every procedure.
  • Server: mint short-lived mkit-url-token:v1 object URL tokens with IssueObjectUrl, signed by a dedicated deployment key, with a two-phase verification API and a key-set renderer for HTTP object serving (WP-2.11).
  • Server: enforce owner-signed write grants under Multi/Owner, including 0x namespaces, stored-epoch checks, and grant-scheme discovery. Adapter grant flags follow in WP-1.30b.
  • Connect client: detect bounded 402 admission challenges and report payment receipts without exposing their values (WP-3.10). TransportError is now non-exhaustive, a breaking change for exhaustive downstream matches.
  • Connect client: run a trusted, user-configured admission helper once and retry admitted writes with strictly filtered headers (WP-3.11).
  • Connect client: sign repository reads with auth v2 on each attempt, including the framed DownloadPack request. Add a grant-source API and local selection logic; the user grant store follows in WP-2.13.
  • Transport: add the GetGrantEpoch, SetGrantEpoch, SetRepoVisibility and IssueObjectUrl messages and RPCs; the server answers unimplemented until WP-2.8, WP-2.9 and WP-2.11 (WP-2.2).
  • Transport: add the AdmissionChallenge and Challenge error detail messages with pinned binary, protobuf JSON, and Connect error goldens (WP-3.1).
  • Specify server takedown and redaction notices: global content blocklist, delta-safe ref and pack rewrites, tombstones, preservation and restore, signed DSSE notices on Connect and HTTP 451, and additive transport and hook fields. Runtime enforcement follows in later server work packages.
  • Specify server-signed storage receipts for committed live ref advances and lease changes, with a published receipt-and-notice key list and writer-view retrieval. Add receipt fields to Connect ref-write responses and an optional admission external_ref; server signing and delivery follow in WP-5.8.
  • Server: AdvanceRefs consumes verified upload tickets atomically with ref publication, outcomes and repository membership. Missing packs after upload close their tickets with Aborted(PACK_MISSING). UpdateRef and AdvanceRefs now support conditional ref deletion; direct writes with admission reservations fail closed until WP-3.3.
  • Server: add stateless UploadPart and CompleteUpload, authenticated part receipts, and a multipart blob-store interface with a working memory backend. Multipart BeginUpload tickets now carry opaque storage sessions. The blob-key API separates upload markers from pack keys; FS and R2 multipart storage follows in later work packages.
  • Server: default Multi-addressing quota now counts namespace-wide usage in fixed windows: exact in each ref shard, reconciled every 60 seconds into the coordinator and checked from an unguarded local view. Coordinator uploads charge that total exactly. Single-addressing keeps its namespace cap off; on D34 its signer quota is per signer and branch. With successful scheduled rollups, other-shard lag is bounded by their admission rate times 3R. Worker timer registration and D34 quota conformance follow in WP-1.26b.
  • Server: add write-once repository object-index keys and binary values, deterministic row planning, and membership-gated lookup APIs for indexed mode.
  • Connect client: poll typed pending AdvanceRefs verification replies with clamped waits, nonce reuse and envelope renewal, progress and cancellation. Ticket deadline threading follows in WP-1.17. API separates upload markers from pack keys. FS multipart storage now stages verified parts durably, completes through the verifying pack sink and sweeps seven-day-old sessions; R2 follows in a later work package. The trait adds begin_multipart_for_ticket so the FS session directory uses the ticket id.
  • Server: ticketed UploadPack now verifies the ticket before reading data, streams the full pack without metadata writes, and leaves a content-addressed upload marker for later ticket consumption. The advertised BeginUpload threshold is enforced; non-default admission requires auth v2 and ticket keys.
  • Server: add periodic per-Durable-Object logical snapshots to a dedicated R2 BACKUPS bucket, plus Fresh-only portable restore with epoch advancement, relay re-keying and coordinator recovery. In-place logical restore is deferred to the admin API; use Workers PITR for recovery of the same deployment. Deploying this Worker now requires the private mkit-vcs-backups bucket bound as BACKUPS and a 35-day backups/ lifecycle rule.
  • Specify the server's published view and quarantine, covering newly reachable file objects and all file entries of added packs. Add inspection phase/id, object kinds, deferral and flagged ids, authority writer-view classification, and discovery field async_inspection = 18. These are additive spec/proto contracts; the inspection implementation follows in later server work packages.
  • Specify storage leases, lifecycle Event webhooks, and fail-closed server GC in SPEC-SERVER; add GetServerInfoResponse.leases and Event proto goldens (WP-5.1a-1). Server enforcement follows in M5.
  • Server: implement authenticated BeginUpload tickets with stateless BLAKE3 MAC tokens, rotation by key id, admission-free live-ticket/member results, open-ticket caps, and byte-identical replay. Configure Worker keys with TICKET_KEYS. The internal legacy UploadPack session API is now open_upload; ticketed uploads follow in WP-1.9b.
  • Server: Worker console JSON metrics and info/warn/error tracing, with latency observations sampled at 1-in-100. Physical storage pressure alerts at 70%/90% of the soft limit run after committed Worker puts and every 60 seconds. Metrics::gauge has a provided no-op default; P-24 is now mkit_server_partition_full_total{kind}.
  • Worker: add Durable Object classes for D34 coordinator, ref, repository/ref-name index and content partitions, retaining RefStore for single deployments; reject foreign partition kinds and preserve alarms scheduled while a timer tick awaits I/O. Deployment vars now select single (default) or d34 with a root sharding marker guard that caches settled results, retries transient storage errors and re-checks config changes; placement is deployment-wide and jurisdiction is fixed for its lifetime.
  • Server D34 ref writes now hold coordinator epoch leases, with backend commit deadlines, guarded revocation pushes and kind-1 expiry sweeps. Creation and renewal cost four store calls; usable leases keep steady writes at two. Recovery explicitly records a lease-table holdoff before revocation completes. Single sharding and M1 reads retain their existing behavior. Grant RPCs, read renewal, visibility, and backup restore integration follow in later WPs.
  • Add partition timers with guarded atomic handlers, fair tick budgets, SQLite timer heads, and Durable Object alarm multiplexing. Production handlers register as their work packages land.
  • The SQL store schema moves to version 2 (an index-only migration, applied on open to Durable Objects). A binary built before it refuses a migrated database, so roll back only to a version-2 binary.

Verifier kit. First-class commit-hash verification for an untrusted object set or a few-KiB disclosure bundle: CLI mkit prove, mkit verify-proof, mkit closure export, and mkit closure verify; wasm exports verify_disclosure / verify_closure_* (and related primitives) in @officialunofficial/mkit-wasm; specs SPEC-MERKLE-OBJECTS §5 and SPEC-DISCLOSURE; user guide docs/VERIFY.md. BMT proof bytes are commonware-identical (commonware_storage::bmt::Proof at the pinned train).

  • Worker launch profile is now LAUNCH_PROFILE=paid-workers. uno remains an accepted deprecated alias and logs a startup warning. Takedown startup accepts configured purge delivery through signed HTTPS or an embedder-supplied PurgeSink; SPEC-SERVER §18 now describes both delivery options.
  • (server) Track a coordinator relay watermark per ref shard, retain expired lease rows while their outboxes are undelivered, and expose the namespace minimum and active shard table for GC and takedown (WP-1.23c). Restore resets maxima and fences watermark reads pending reconciliation. The unshipped LeasedShard V1 encoding gains watermark and sweep due fields.
  • (client) Validate the remote URL path as a repository identity and carry X-Repository on every Connect RPC, including anonymous reads. Empty paths address default; reads against a different configured single-repository identity now fail. Repository-not-found errors name the identity and origin.
  • (client) Discover and cache atomic advance automatically from GetServerInfo, with conservative defaults for legacy or unavailable servers. SQLite and Durable Object pushes may now re-baseline the packmap chain. Concatenate paged ListRefs responses with ordering and termination guards.
  • (core) Add defaulted Transport::download_pack_via_ref, download_blob_via_ref, and pack_exists_via_ref methods. Connect uses their validated X-Mkit-Ref hint for packmap-driven downloads; other transports preserve their existing behavior. Add defaulted repository_address for remote error context.
  • (client) Retry Connect aborted responses as temporary failures (503). Missing ref reads return None; missing pack checks return false.
  • (server) Scope Multi PackExists and DownloadPack to repository membership, with the optional unsigned X-Mkit-Ref hint resolving unrelayed additions in the same repository's ref shard. Invalid hints are ignored; Single reads and the Multi upload guard retain their behavior (WP-1.23b).
  • (worker) Register relay delivery on RefShard, with bounded target calls and plan-specific alarm budgets. Coordinator relay watermarks follow in WP-1.23c (R-106).
  • (server) Persist source relay scan progress in rs 00, with a cap of 32 failed targets per cycle. Relay fires inspect past blocked targets, pause at the target budget, delete every delivered row in the guarded checkpoint, and deliver the decodable prefix before corruption. Guarded scan checkpoints and exact empty-value relay timer reschedules can use SQL's soft-capacity reserve; the timer exception keeps the next fire immediate after progress on a full shard. Ordinary puts still fail at the cap.
  • (server) Add source-side outbox relay kind 3, ordered target batches, persistent per-source rh deduplication watermarks, atomic pre-delivery hooks, and bounded source cleanup (WP-1.23a). Relay writers now call relay_at(now_ms) to stamp rows and commit their kick timer; RelayV1 gains mandatory at_ms in place before deployment. Writer chunking keeps each row within target-batch limits, and the local relay watermark and 60-second lag warning prepare later readers. Worker registration follows in WP-1.23b; the coordinator watermark follows in WP-1.23c.
  • (server) Implement unauthenticated GetServerInfo deployment discovery with validated upload limits, namespace/admission policy, store capabilities and private caching for 60 seconds. Repository headers never affect the response (WP-1.6). A configured maximum pack size above the advertised resumable-upload limit (8 MiB parts × 10,000 parts, about 78 GiB) is refused.
  • (server) Multi addressing now defaults to an empty namespace allowlist and owner writes. Namespace denials and non-owner writes return permission_denied before allocation. Authorizer hooks can be additional checks or explicit authority sources, with owner facts passed to Authorize and Admit. Startup refuses incompatible write policies, an open authority hook, and any with default admission unless explicitly overridden (D27; WP-1.5). Embedders select these policies in core; adapter multi-mode configuration follows in WP-1.30.
  • (server) Add pure ref-shard planners and strict version-1 codecs for upload tickets, reservations, local pack membership and outcome/relay queues (WP-1.7). Shared counters and outbox sequence/backlog edits carry snapshot guards; ticket expiry kind 2 is allocated without an RPC or timer handler. Storage layout version stays 1.
  • (core) Add pack::window: a synchronous sans-IO pack reader with bounded entry buffers, 64 KiB–64 MiB range windows, and checksummed resumable cursors. Entries remain provisional until the trailer and optional pack id pass at Done. Lazy cursor commitments bind the current window prefix on resume; callers keep sources immutable, and completed windows are not re-read. With no requested id, the first run prefetches the trailer windows and persists their digest as an anchor. Native zstd and wasm32 ruzstd share decoding with PackEntries; allocation failures return PackfileTooLarge.
  • (server) Validate X-Repository before authentication and storage access. Single deployments retain headerless reads and signed writes naming their configured identity; another valid identity returns not_found, and a malformed identity returns invalid_argument. Worker AUTH_REPOSITORY now require the SPEC-TRANSPORT-CONNECT §7.4 grammar (previously any printable ASCII was accepted); default remains valid. Embedders can construct Addressing::Multi to route refs and replay state by namespace and repository. Multi pack RPCs return unimplemented until repository membership lands in WP-1.10. Addressing::resolve returns a ResolvedRepo, stored on Authenticated::repo(), without changing the adapter-facing authentication API.
  • (cli) mkit serve <path> runs on mkit-server: its engine is mkit_server::ssh::serve_session over the pipeline with the .mkit layout stores (FsBlobStore, FsLayoutStore), driven by futures::executor::block_on, so the CLI still builds no async runtime. Its own protocol code is gone. The wire is unchanged: the two golden sessions captured from 0.4.2 (rust/tests/golden/ssh-serve/) reproduce byte for byte through the binary. mkit-cli now depends on mkit-server (features ssh and fs only), which is first published with the 0.5 release. Behavior changes:
    • Idle timeout. mkit serve ends a session after --idle-timeout-secs (default 60; 0 disables it) without a byte from the client, answering Error{INVALID_REQUEST, "idle timeout"} and exiting 76 (SSH-SECURITY §4, §7). It bounds client silence only: an upload that keeps sending never trips it; one that stops midway is discarded. A new, optional --max-session-secs (default 0, off) caps the whole process, which also bounds a client that trickles bytes or stops reading. Both flags accept at most 604800 (7 days).
    • Refs only under refs/. A ref name outside refs/ (main, heads/main) that mkit serve used to store at <root>/<name> is refused by name ("ref name must start with refs/ (… see the migration notes)"). Such files are left in place and never reported to clients; docs/CLI.md, "Refs outside refs/", gives a find/mv recipe for an operator to locate and move them by hand. See the mkit-server entry below.
    • Crashed uploads are swept. At startup, when no other mkit serve or server process holds serve.lock, upload temp files (packs/.<hex>.tmp.<pid>.<seq>) at least an hour old are removed.
    • A root marked as served from external metadata storage is refused (exit 78). The default CLI graph is checked server-free by the new scripts/check-cli-baseline.sh (in just ci-scripts; INVARIANTS "The default mkit CLI is server-free").
  • (server) The pipeline serves only ref names under refs/ (mkit_server::refs::is_served_ref_name; SPEC-REFS §2), on every binding: ReadRef, UpdateRef and AdvanceRefs refuse any other grammar-valid name with invalid_argument "ref name must start with refs/ …" (INVALID_REQUEST on the ssh wire), where they used to store it (in .mkit/server/rows/ on the .mkit layout, invisible to the CLI). ListRefs prefixes are unrestricted; one outside refs/ lists nothing. Normative in SPEC-REFS v3 §2 (the §2 namespace list now includes refs/mkit/packmap/), SPEC-TRANSPORT §4.2.1 and SPEC-TRANSPORT-CONNECT §5; new wire-suite case refs.non_refs_prefix_rejected. New FsBlobStore::sweep_stale_uploads. SemVer: unreleased API.
  • (transport-file) Every ref write through FileTransport (update_ref/write_ref, and LockedRefs::update_ref, delete_ref, write_file, remove_file) refuses a root carrying SERVER_META_MARKER (.mkit/server-meta), the marker a SQLite-metadata server deployment writes, with the new RefFileError::MetaElsewhere (a TransportError::RemoteError on the Transport verbs). That root's refs live in the server's SQLite database, so a local mkit push to a file:// remote or mkit serve can no longer write a second, diverging copy. Reads and pack uploads are unchanged. SemVer: additive (new constant and a variant of the #[non_exhaustive] enum); behavior change only on marked roots.
  • (vcs-worker) apps/vcs-worker is a thin deployment of mkit-server-worker: its fetch handler and RefStore Durable Object call the new mkit_server_worker::adapter, which serves mkit-server's pipeline over R2 and Durable Object SQLite, streaming request and response bodies (no whole-pack buffering; DownloadPack sends 800 KiB chunks instead of one). Class RefStore, binding REFSTORE and bucket STORAGE are unchanged, with no wrangler migration; the Durable Object now keeps a kv table and ignores the old refs, write_quota and authenticated_operations tables (never deployed; no migration). Wire changes, all SPEC-mandated: a reused nonce for another operation is invalid_argument (was an uncaught 500); a 33-byte expected_id is invalid_argument (was failed_precondition); ListRefs matches its prefix at a path-component boundary; a gzip-compressed unary response (e.g. a large ListRefs) is no longer compressed a second time by the runtime; an upload stream that runs past its declared size and past 64 MiB is invalid_argument (was resource_exhausted); storage failures surface as internal/unavailable instead of invalid_argument "refstore …"; a missing AUTH_AUDIENCE or AUTH_REPOSITORY makes every RPC unavailable (was: writes only). New var WORKERS_PLAN (free, the default, or paid) caps the Durable Object store for the plan. The generated code, build.rs and the duplicated modules are gone; scripts/vcs-worker-conformance.sh runs the wire suite against it under wrangler dev.
  • (core) mkit_core::refs::validate_ref_name now also requires a name of at most MAX_REF_NAME_BYTES (512) bytes, per SPEC-REFS v2 §3, so every transport and every new local ref refuse a longer one. Creating or writing a local branch, tag or remote-tracking ref over the bound fails with the new RefError::RefNameTooLong, whose message names the limit. Reading, listing, resolving and deleting a local ref that already exists check only the grammar (the new validate_ref_name_grammar), so a longer ref written earlier stays visible and can be deleted or renamed with mkit branch -d/-m. New: MAX_REF_NAME_BYTES, check_new_ref_name, validate_ref_name_grammar.
  • (core, cli) A new local branch name is at most MAX_BRANCH_NAME_BYTES (494) bytes and a new tag name at most MAX_TAG_NAME_BYTES (502), so their wire names (refs/heads/<b> and refs/mkit/packmap/<b>, refs/tags/<t>) fit the 512-byte bound; the error names the derived limit and why (RefError::RefNameTooLong now carries a RefNameKind). A push of an older, longer branch fails with "ref name too long", naming the wire name (check_pushable_branch). The ssh and enc clients skip a listed ref whose name is over the bound (mkit_rpc::list_response_refs) instead of failing the listing, as the file, memory, s3 and http clients do.
  • (refs, rpc, server) SPEC-REFS v2: a ref name is at most 512 bytes (§3). mkit_rpc::MAX_REF_NAME drops from 4096 to 512 (it is now mkit_core::refs::MAX_REF_NAME_BYTES), so the ssh and enc clients refuse a longer name or ListRefs prefix before sending. mkit-server refuses one on ReadRef/UpdateRef with invalid_argument "ref name too long" (INVALID_REQUEST on the ssh wire), and a ListRefs over the .mkit layout skips a longer legacy ref file with a warning. mkit serve over ssh used to accept names of any length. Pre-production policy: no migration.
  • (server) mkit-server's ListRefs matches its prefix at a path-component boundary and strips the prefix plus its /, as SPEC-REFS §4 and mkit serve do (refs/heads and refs/heads/ list main; refs/heads/ma lists nothing). A listing of the .mkit layout skips a ref file that holds no ref id, with a warning, instead of failing. FileTransport::list_refs_strict is replaced by list_ref_files, which reports such a file as None.
  • (core) Pack readers enforce SPEC-PACKFILE §3.3's "one zstd frame" rule. A 0x03/0x04 payload holding two concatenated frames, a skippable or legacy-magic frame, or trailing bytes after the frame now fails with PackError::ZstdDecompress. The C path (zstd::bulk::decompress) used to decode concatenated frames and skip skippable ones. mkit's PackWriter never produced such payloads. Pre-production policy: no compatibility path.
  • (core) list_refs/tag/remote listing read each ref with a single stack-buffer read (open+read+close) instead of fs::read's extra statx and EOF probe: −23% at 10k refs, −25% at 100 refs (refs_ops bench).
  • (core) SPEC-DISCLOSURE v2: every Step and chunk header carries a mandatory 32-byte inner_root (bare BMT root of the parent Tree / ChunkedBlob). Bundle version byte is 2; version 1 is a typed UnsupportedBundleVersion(1) with no compatibility decoder. The verifier wrap-checks the field against the trusted id before use, then requires the proof fold to equal the declared root. SemVer: breaking for bundle bytes (pre-release format, no migration); additive for APIs (Disclosed.step_inner_roots / Disclosed.chunk_inner_root; Step.inner_root).
  • (core) ops::diff::myers_changed (the line-level Myers edit script behind unified_hunks/enumerate_hunks — diff, show, add -p, and merge_blob_3way's 3-way diff) now elides any common leading run of identical lines before running its O(ND) core on the (usually much smaller) remainder — the same "diff only the changed region" step git's xdiff and every other practical diff engine apply before their own O(ND) search. A real edit almost always leaves most of a file as a shared prefix (a single changed line, an appended block); the old code always sized its v/backtrack-trace state off the whole old.len() + new.len(), regardless of how small the actual edit was. New elision logic in myers_changed finds the shared prefix with one cheap linear scan, runs the renamed myers_changed_core (the previous function body, unchanged) on just the post-prefix slices, and splices the result back into full-sized change-flag vectors — this is exactly the core's own deterministic first step (at d = 0 there is only one diagonal, k = 0, so no tie-break is involved; it always greedily extends this same leading run before doing anything else), so skipping straight to the subproblem is provably identical to letting the core discover it itself.

    An independent review caught a real bug in the first version of this change before it merged: it also elided a common trailing run, matching backward from the ends of both sides. That's unsound in general — unlike the prefix, the trailing snake the core's own backtrack lands on depends on tie-breaks made throughout the whole search (v[idx(k-1)] < v[idx(k+1)]), so whenever repeated or colliding lines near the tail admit more than one equally-minimal alignment, greedily matching from the end backward can commit to a different one than the real algorithm would have — found with a concrete counterexample (old = ["a"], new = ["b", "a", "a"]: the core matches old[0] to the first new "a"; backward-suffix-matching commits to the second) and confirmed by exhaustive brute-force diffing over small alphabets. The apply_hunks_subset round-trip property (proptest_hunks_roundtrip_with_shared_affix, kept below) could not catch this — any minimal edit script round-trips correctly regardless of which alignment it picks — so a new proptest_myers_changed_matches_unelided_core compares myers_changed's output directly against myers_changed_core run un-elided on the same input over a 3-symbol alphabet chosen to maximize boundary repeats/collisions; it failed immediately against the buggy trailing-elision version (minimal counterexample old = [0, 0], new = [0, 1, 0, 0]) and passes with suffix elision removed. Suffix elision was dropped entirely rather than patched to match the core's tie-break, since doing that correctly would mean re-deriving part of the backtrack itself — not a shortcut worth taking for one side of the win.

    New cargo bench -p mkit-benches --bench diff_edit_script suite (single-line edit and 200-line append against 1k/10k/100k-line files, plus a no_common_affix control with no shared prefix at all, 4-core host, --quick): 10k-line append 27.87ms → 0.23ms (119x), 100k-line append 297.4ms → 2.27ms (131x) — the appended-block case is the flagship win, since the entire original file elides as a shared prefix and only the new tail ever reaches the O(ND) core; 100k-line single-line edit 9.04ms → 5.91ms (−35%, the remaining cost is split_lines/compact_changes/script_from_flags's own O(n) work, plus the second (post-edit) half of the file that prefix-only elision can no longer skip). no_common_affix (1k/2k lines, kept small since it's O((n+m)^2) independent of this change — Myers' own worst case, a genuinely unrelated cost) is unaffected within run-to-run noise (40-42ms/170-172ms both before and after), confirming the extra linear scan costs nothing when there's no shared prefix to find. All 861 mkit-core tests pass (including both new property tests), cargo clippy -p mkit-core -p mkit-benches --all-targets -- -D warnings and cargo fmt --check are clean. SemVer: none — myers_changed/myers_changed_core are private to ops::diff; every public diff/merge API's behavior and output are unchanged.

  • (core) pipeline FastCDC chunk-cutting with chunk-hashing in worktree::store_large_file_streaming_with (the streaming ingest path behind mkit add/status/diff on any file above CHUNK_THRESHOLD, 1 MiB). The existing implementation was strictly phase-serial: cut a batch of up to STREAM_HASH_BATCH (64) chunks on the calling thread, then block waiting for the caller's hash_chunks callback (typically a rayon fan-out hashing/staging each chunk) to finish before cutting the next batch — cutter and hashers were never both busy at once, even though cutting batch N+1 has no data dependency on hashing batch N, only on the reader position, which the cutter alone advances. Native builds (#[cfg(not(target_arch = "wasm32"))]; mkit-core stays wasm-clean, and wasm32 has no threads to pipeline across) now run the cutter on its own std::thread::scope worker, handing batches to the calling thread over a rendezvous (mpsc::sync_channel(0)) channel — a zero-capacity channel's send blocks until the matching recv, so the cutter can have at most one batch fully built ahead of the one currently being hashed, capping the extra memory this adds at one more STREAM_HASH_BATCH (matching this module's existing "independent of file size" bound) while still overlapping the two phases fully.

    An early review pass caught a real deadlock in the first version of this change: if the receiving side stops calling recv as soon as hash_chunks errors (the natural ?-propagation shape), the cutter thread's next send — for a batch it already cut, or is still cutting — has no buffer to land in and blocks forever waiting for a recv that will never come, hanging std::thread::scope's join on that thread and the whole call with it. Fixed by having the receiving side keep draining (discarding) the channel after recording the first error instead of returning immediately, so every future cutter send still succeeds (or the cutter hits its own error/EOF and exits on its own) and the channel always disconnects, letting the receive loop terminate on its own. New streaming_pipeline_errors_promptly_when_hash_chunks_fails_on_an_early_batch pins this directly: three full batches (so the cutter still has more to send after the one that triggers the very first error), run on its own thread with a bounded recv_timeout so a regression fails the test instead of hanging the suite.

    Verified with a controlled in-process A/B (store_large_file_streaming_batched, the original phase-serial loop, vs. the new pipelined path, interleaved on identical in-memory fixtures with a rayon-style scoped-thread hash_chunks, median of 10 runs each, to cancel this shared host's cross-process noise) at sizes spanning chunk_hash_fanout's existing file/<n>_mib bench: 8 MiB is a wash (only ~2 batches worth of data, little to overlap), 32 MiB ~5-9% faster, 128 MiB ~5-10% faster — growing with size as expected, since more batches means more opportunity for the two phases to overlap; this is the shape of the "Add and commit one 1 GiB file" and "Re-add an unchanged 100 MiB file" rows on the performance page. Content addresses are verified byte-identical between the two paths on every run. All 860 mkit-core tests (up from 859) pass, cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --check are clean, and a wasm32-unknown-unknown build of mkit-core (--no-default-features) succeeds using the unchanged phase-serial fallback. SemVer: none — store_large_file_streaming_batched/store_large_file_streaming_pipelined are private; store_large_file_streaming_with's public signature gains a + Send bound on its R: Read reader type parameter (satisfied by every existing caller — io::Take<fs::File>, io::Cursor<Vec<u8>>> — so this doesn't break any in-tree usage) and its documented behavior/output are otherwise unchanged.

  • (core) worktree::content_eq (the "did this file's content actually change?" check behind add, status, diff, and merge once two sides' object ids already differ) gained a fast path for the ChunkedBlob-vs-ChunkedBlob case — every file above CHUNK_THRESHOLD (1 MiB). The old code always fully reassembled and byte-compared both sides, chunk by chunk, even when most chunks were byte-identical between versions: appending 1 MiB to a 100 MiB file — the flagship "commit a 1 MiB change to a 100 MiB file" row on the performance page — re-read and re-verified the whole 100 MiB on both sides just to answer "not equal". New chunked_content_eq walks both manifests' chunk-hash lists with two pointers, skipping any run of chunks that share a hash with no read at all — identical hash means identical bytes by construction, the same trust content_eq's pre-existing a == b whole-object fast path already relies on, just applied per chunk. Only chunks that actually diverge are read and byte-compared, with the walk able to resync once ids match again at the same aligned offset (pinned by chunked_fully_misaligned_but_equal_content/chunked_shared_prefix_then_misaligned_equal_suffix and the pre-existing large_inline_fixed_and_cdc_content_agree, which compares two independently-chunked manifests — fixed 64 KiB blocks vs. real FastCDC — of the same content). An append needs zero chunk reads at all: FastCDC's cut points never depend on bytes past them, so every pre-existing chunk still lines up by hash and the walk runs straight to the new tail. ma.total_size != mb.total_size is also checked up front without reading anything, trusting the manifest's declared size the same way LoadedBlob::len's doc comment already documents (every reassembly path enforces it via ChunkedBlob::check_reassembled_size, so it cannot be wrong on anything durably written through mkit's own writers) — a manifest deliberately constructed to violate that, whose entire chunk sequence still lines up by hash with its counterpart, slips through unnoticed by this fast path specifically (pinned, not just asserted, by chunked_wrong_shared_total_size_is_not_detected_when_ids_fully_match); one whose chunks diverge anywhere is still fully read from the first divergent chunk on, so store-level corruption or a missing chunk there still surfaces as an error, same as before.

    New cargo bench -p mkit-benches --bench content_eq suite (append/edit-middle-byte/truncate-1MiB against an 8 MiB and a 32 MiB FastCDC-chunked base, 4-core host, --quick): 8 MiB append 13.78ms → 3.51ms, edit 11.05ms → 3.70ms, truncate 11.18ms → 3.35ms; 32 MiB append 44.80ms → 12.42ms, edit 43.80ms → 11.99ms, truncate 43.76ms → 10.60ms — roughly 66-76% faster wall-clock across every mutated-content case at both sizes (the identical series, which hits the pre-existing a == b fast path before ever reaching chunked_content_eq, is unchanged, as expected). New tests: direct coverage of chunked_content_eq's merge logic against small hand-built manifests (all-ids-match, fully-misaligned-but-equal, shared-prefix-then-resync, append via total_size, same-total-size-different-content, split-differently-but-different-content, empty manifests, the documented total_size gap above), a real-FastCDC insertion test that confirms at the manifest level (shared first/last chunk, differing middle) that a genuine divergence-then-resync actually happens before checking content_eq against ground truth, and content_eq_real_chunked_mutations_match_ground_truth, which runs content_eq over real FastCDC-chunked append/edit/truncate mutations of a 3 MiB fixture and cross-checks every result against a direct byte comparison.

    An independent code review (correctness + reuse/simplification pass) caught a real gap in the first version of this change before it merged: chunked_content_eq only ever compared the two manifests' declared total_size fields against each other, never against either side's own real chunk bytes, so two manifests whose chunk sequences diverged everywhere (no id ever matched, so nothing was ever skipped) but happened to declare the same wrong total_size were reported merely "unequal" instead of raising ChunkedBlobSizeMismatch the way the old byte-cursor walk always did. Fixed by tracking, per side, the real bytes actually read (as opposed to skipped by id) and whether that side had any chunk skipped at all: a side with zero skips has every one of its chunks read regardless, so its accumulated real length is now checked against its declared total_size before returning, erroring exactly as before if they disagree. This narrows the documented trust gap to only what it was always meant to cover — a side that skipped at least one chunk by id, which is the one case this fast path cannot validate without giving up the read it just avoided. New chunked_wrong_total_size_is_detected_when_no_chunk_is_skipped and chunked_wrong_total_size_on_one_side_only_is_detected pin the fix directly (both previously-missed error cases; both now assert ChunkedBlobSizeMismatch). The review also found three call sites in this file (content_fingerprint, ContentCursor::remaining, and chunked_content_eq) had each hand-rolled the identical "read a manifest chunk, require Blob" match; factored into one shared read_blob_chunk helper (read_chunk, the pre-existing WorktreeError-domain sibling with its own hash-and-type-qualified error text, is kept separate since unifying it would have changed that error's wording). No wire-format or public-behavior change beyond the bug fix itself — every other case this fast path was already correct for is unaffected, confirmed by the full pre-existing test suite passing unchanged alongside the new tests. All mkit-core tests (842, up from the base branch's 839) pass, cargo clippy --all-targets -- -D warnings and cargo fmt --check are clean, and the content_eq bench numbers above are unaffected (re-measured after the fix). SemVer: none — chunked_content_eq, read_blob_chunk, and ContentCursor::from_object are private to worktree::blob; content_eq's public signature and documented error behavior are unchanged (the fix restores documented behavior for the no-skip case rather than changing it).

  • (core) replace delta::encode's block-hash index (HashMap<u64, u32>, std's default SipHash-1-3 hasher) with the same map keyed by rustc_hash::FxHasher, seeded per encode() call. The index's keys are already the output of encode's own FNV-1a block_hash over each 16-byte block of base, so SipHash's extra per-byte cost buys little — if the replacement is still reseeded per call: an earlier version of this change used a bare, fixed-seed hasher (via BuildHasherDefault/the crate's own bare FxBuildHasher, both always starting from the same compile-time state), caught in review as a real regression — block_hash is itself unkeyed, so an attacker who controls base's bytes could solve for many distinct blocks whose block_hash outputs collide in the same table bucket under a known seed, degrading the index's build from O(n) to O(n²): the exact hash-flooding attack SipHash exists to prevent, reopened. encode now seeds via rustc_hash::FxSeededState::with_seed, drawing a fresh random usize per call from std's own randomized hasher (random_seed, avoiding a new rand/getrandom dependency for one value per call) — denying an attacker the one thing that attack needs: a bucket mapping it can predict in advance. Also switched from a hand-rolled reimplementation of the FxHash algorithm to the rustc-hash crate directly (already resolved transitively via mkit-cli's reqwest → quinn chain, caught in the same review as needless duplication of an already-available, better-tested implementation) — rustc_hash::FxSeededState (added upstream specifically for this "seed a fast hasher yourself" use case) supplied the seeding API a bare FxHasher/BuildHasherDefault doesn't, so no hand-rolled BuildHasher wrapper was needed either. cargo bench -p mkit-benches --bench delta_plan_fanout -- --quick (sequential series, 4-core host, near-duplicate 64 KiB chunks) with the final, reseeded implementation: 32 candidates 3.25ms → ~2.9-3.0ms, 64 8.50-8.65ms → ~6.15-6.23ms, 128 14.0-17.0ms → ~12.7-12.9ms, 256 candidates 36.6-37.1ms → ~25.7-26.3ms (~29-30% faster) — smaller than the fixed-seed version's initial (unsafe) ~36-39%, since reseeding costs one RandomState construction per encode() call, but still a clear, real win. New random_seed_differs_across_calls_and_seeds_a_usable_map test pins this module's own contribution — FxHasher's mixing quality is rustc_hash's tested contract, not this module's to reverify — confirming two random_seed() calls differ (the load-bearing security property) and that FxSeededState wires a seed through consistently enough for a HashMap to find keys it just inserted. No wire-format or public-API change — FxIndexMap/random_seed are private to delta.rs, and encode's output is byte-for-byte identical to before (all mkit-core tests, including delta's roundtrip and corruption-rejection suite, pass unchanged). SemVer: none.
  • (core,cli) batch Ed25519 signature verification in remote_dispatch::packmap::verify_new_object_signatures (clone/pull/fetch's post-download check, issue #692). Every commit/remix/tag a fetch introduced was checked with a separate verify_strict call — one scalar-multiplication-plus-comparison per object. New mkit_core::sign::verify_batch (behind a new, non-default batch-verify feature) instead runs ed25519_dalek::verify_batch's randomized-coefficient multiscalar-multiplication check over many signatures in one pass — strictly less total scalar-multiplication work than one verify_strict per signature — at the cost of not learning which entry failed on a batch rejection; verify_new_object_signatures falls back to the pre-existing per-object loop whenever the batch check fails (or wasn't attempted) to locate and report the exact offending hash, unchanged from before this commit. ed25519-dalek's batch equation deliberately omits two of verify_strict's malleability checks (see its README's "Malleability" section for the public-key half; the signature-R half is the same class of check, just undocumented as a batch caveat) — verify_batch performs both itself, per entry, before running the batch equation: VerifyingKey::is_weak on the public key, and a direct curve25519-dalek small-order check on the signature's R. Each is backed by a dedicated regression test constructing a genuine forgery specific to that check (a weak identity public key with an all-zero signature; and, isolating the R check from the public-key one, a real full-order key's own secret scalar used to solve [s]B = R + [k]A for R = identity) — both confirmed to satisfy ed25519-dalek's own loose verify (proving the gap is real, not just theoretical) and confirmed load-bearing by deliberately disabling each precheck in turn and watching the corresponding test fail.

    A single whole-slice batch call is single-threaded, so it doesn't automatically beat the existing per-object rayon fan-out at fetch sizes large enough to keep every core busy. cargo bench -p mkit-benches --bench verify_fanout (new batch/batch_parallel series, 4-core host): at 256 entries, one whole-slice verify_batch call (11.7-12.1ms) is slower than the pre-existing per-object rayon fan-out (8.9-9.6ms) — batching's ~2x reduction in total scalar-multiplication work doesn't make up for using only one of four cores. Splitting the batch into one sub-batch per rayon worker thread instead (new verify_batch_parallel, used whenever verify_new_object_signatures would otherwise parallelize) combines both effects and consistently beats plain rayon fan-out from 16 entries up: 16 entries 1.05-1.08ms → 0.97-1.05ms, 32 1.7-1.71ms → 1.53-1.6ms (~6-10%), 64 2.96-3.03ms → 2.47-2.48ms (~17-19%), 128 5.19-5.26ms → 4.13-4.30ms (~18-21%), 256 8.91-9.64ms → 7.08-7.42ms (~20-24%) — the gap widens with size, matching the fixed 512-entry chunk cap large fetches already use. SemVer: additive — mkit-core gains sign::verify_batch behind the new non-default batch-verify feature (mkit-cli enables it explicitly on its mkit-core dependency; wasm/Worker builds and other native consumers such as contrib/signers/* are unaffected since they don't request it and it isn't default). No existing public API changes.

  • (core,cli) parallelize list_refs's per-ref read across rayon. refs::collect_refs's directory walk read and decoded each ref file (a separate fs::read + decode_ref_wire, one small-file syscall sequence each) sequentially, one at a time — the same "independent per-file work, no fan-out" gap add's worktree hashing and remote_dispatch's pack/delta/signature fan-outs closed elsewhere, just never closed here (issue tracked from the same family as those). New refs::list_refs_with(layout, read_batch) splits the existing behavior into a sequential directory walk (cheap, metadata-only, stays sequential) that collects RefCandidate { name, path } entries, then hands the whole batch to a caller-supplied read_batch callback — the same "collect first, batch-callback second" shape transfer::plan_pack_with/worktree::store_large_file_streaming_with already use. read_batch returns one RefReadOutcome (Unreadable, dropping the entry, or Decoded(Option<Hash>), keeping it — matching the old inline behavior exactly: an unreadable file was silently skipped, a readable-but-malformed one was kept with Ref::hash: None) per candidate, a cardinality mismatch failing closed as a new RefError::RefBatchLengthMismatch rather than silently desyncing names from hashes (mirroring WorktreeError::ChunkBatchLengthMismatch/StoreError::DeltaBatchLengthMismatch's existing contract). list_refs/list_remote_refs/tag listing keep their exact old sequential behavior by default via sequential_read_batch, itself now just candidates.iter().map(read_ref_candidate) — the new refs::read_ref_candidate is the single read-and-decode definition shared by that default, mkit-cli's fan-out, and the fan-out bench, so the Unreadable-vs-Decoded(None) policy has exactly one place to drift from. Three new tests pin list_refs_with's contract directly (sequential-batch equivalence, the Unreadable-vs-Decoded(None) distinction, the length-mismatch error).

    mkit-cli gains commands::list_refs_parallel/list_tags_parallel/list_remote_refs_parallel (sharing one fanout_read_batch closure), wired into every CLI call site that previously read refs::list_refs/list_tags/list_remote_refs directly (branch, tag, ref, show-ref, for-each-ref, git's git-export tag/branch export, remote_dispatch's push and pull) — a command like for-each-ref or show-ref that lists heads, tags, and every remote's tracking refs in one call now gets the parallel win on all three namespaces, not just the one that happened to be wired up first. cargo bench -p mkit-benches --bench refs_ops -- list_refs_fanout (new suite): 100 refs 412us → 346us, 1k refs 4.82ms → 2.32ms (2.1x), 10k refs 58.1ms → 27.5ms (2.1x) — a real win even at the smallest tested size, not just at scale. A pre-existing, unrelated bug in this same bench file (refs_ops.rs's fixtures never called refs::init before writing refs, so every refs_update/list_refs case failed outright) is fixed alongside it, since it blocked getting any numbers at all for this change. SemVer: mkit-core gains refs::list_refs_with/list_tags_with/list_remote_refs_with, refs::read_ref_candidate, refs::RefCandidate, refs::RefReadOutcome, and RefError::RefBatchLengthMismatch — new variants in public exhaustive error enums can require downstream match updates; list_refs/list_remote_refs/tag listing's signatures and behavior are unchanged.

  • (core) bound the O(N) store-read cost advance pays on every history-mmr fast-forward publish, instead of re-verifying the entire first-parent chain from scratch every single time. This was a known, deliberately-left-alone cost (see the read_current_descriptor entry below and its own predecessors) — a dedicated research pass (prior art from git's object-verification model, RFC 6962 Merkle consistency proofs, ZFS/btrfs scrub semantics, and the MMR/accumulator literature mkit's own ancestry proof already draws on) confirmed the check is real (it defends against accidental local corruption — bit rot, a bad GC, a torn write — between two publishes; content addressing means corruption is always eventually detectable, never silently wrong, so the only question is when it gets caught) but stronger than any of those reference systems apply by default, and that the one prior attempt to cut it (first_parent_chain_from's chain-splicing fast path, prototyped and reverted in an earlier commit) failed specifically because it dropped re-verification of the reused prefix to zero rather than bounding it.

    New history::ancestry::decide_chain replaces advance's unconditional first_parent_chain(store, target) call. A true no-op (target already the live, already-verified tip) now short-circuits before any store read at all — previously it still paid the full walk before discovering there was nothing to do. A genuine fast-forward (found via new first_parent_suffix_to, which walks backward from target only until it hits the live tip instead of all the way to genesis) always verifies its new suffix in full, but re-verifies the reused prefix only in a bounded, rotating window — max(512, verified_through / 64) leaves — tracked per branch in a new advisory auxiliary file (ScrubState/scrub, SPEC-HISTORY-PROOF §4.5). A full walk still happens, exactly as before, whenever: it isn't actually a fast-forward (new branch, rewrite, reset, unrelated history — first_parent_suffix_to returns NotFound), the rotating window would complete a lap over the prefix, or more than 7 days have passed since the last full verification (matching ZFS's default weekly scrub cadence, more conservative than btrfs's monthly) — and a full walk started for schedule reasons while still a fast-forward correctly keeps the same generation, not a fresh one. Missing, corrupt, or unparseable scrub state is never treated as "already verified" — it always falls back to a full walk, the same fail-safe-toward-more-verification default as every other case. None of this changes what gets persisted: the published chain and root are always byte-identical to what a full first_parent_chain walk would produce, for any given target — only how many of an already-published prefix's objects get re-read from the store on a given publish. This bounds the worst-case staleness of any single leaf's last re-verification at 64 fast-forward publishes or 7 days, whichever comes first, instead of either "every publish" (the old, expensive default) or "never again" (the reverted fast path's actual behavior).

    Six new tests exercise the scheduling directly: a fast-forward's window is provably narrower than the full prefix (fast_forward_scrubs_a_bounded_window_not_the_whole_prefix) yet still publishes byte-identical output; a completed rotation forces a full walk while preserving the generation (scrub_lap_completion_forces_a_full_walk_but_keeps_the_generation); corruption planted outside the current window doesn't fail the next fast-forward but is caught within a bounded number of publishes once the cursor reaches it, never silently forever (corruption_outside_the_current_window_is_caught_within_a_bounded_number_of_publishes); a stale last_full_verify_unix forces a full walk regardless of cursor position, via a test-only clock override mirroring this file's existing FAIL_AFTER fault-injection pattern (stale_scrub_state_forces_a_full_walk_regardless_of_cursor_position); and missing scrub state fails safe toward more verification, not less (missing_scrub_state_forces_a_full_walk_instead_of_trusting_nothing). All existing ancestry tests pass unchanged, including many_sequential_publishes_keep_one_generation_until_a_real_rewrite's 40 sequential single-commit publishes — all below SCRUB_MIN_WINDOW (512), so every one of them exercises the "lap completes immediately, full walk every time, same generation" branch, the same behavior as before this commit at that scale.

    cargo bench's sequential_publish series (100-300 commits) can't show any of this — the 512-leaf minimum window covers the whole prefix in one pass at that scale, so every publish there still does a full walk exactly as before, and fsync dominates regardless (see the entries below on why that bench can't isolate cheaper mechanisms). A new #[ignore]d manual profiling test, profile_scrub_window_vs_full_walk_every_publish (not run in CI), isolates it at a chain length long enough to matter: at 20,000 leaves, 20 sequential fast-forward publishes averaged 71.8ms/publish with the scrub window against 324.7ms/publish with a full walk forced every time (both paths otherwise identical — same fsync/sync_dir sequence, same per-publish MMB rebuild over the full chain, which remains real, unavoidable, unrelated O(N) work this change does not touch) — a 4.5x reduction at this size, growing with N since the shared floor (fsync plus the MMB rebuild) stays roughly constant while the avoided read count grows. SemVer: mkit-core gains RefError-adjacent internals only — decide_chain, ChainDecision, ScrubState, first_parent_suffix_to, SuffixWalk, and the scrub-file read/write helpers are all private to history::ancestry; no public API changes.

  • (core) stop loading the previous publish's whole first-parent chain from disk on every history-mmr publish — advance only ever needed its small, fixed-size descriptor header (repository/full_ref/generation/tip/leaf_count/root), never the up-to-32-MiB chain: Vec<Hash> that follows it in the snapshot file. This follows on from read_current_chain (below), which already stopped rebuilding an MMB from that chain but still read and checksummed the whole file to get it.

    The key realization: advance's comparison logic (no-op detection, fast-forward/generation-reuse decision) only ever compares old's fields against chain = first_parent_chain(store, target) — a fresh, full, store-re-verified walk advance computes unconditionally anyway (unchanged — this remains the deliberate O(N) integrity check, not something this commit touches). Because objects are immutable and content-addressed, that walk is a deterministic function of (store, target): if target == old.tip, chain is byte-for-byte old.chain without needing to check — both are first_parent_chain(store, old.tip), just computed at different times against the same unchanging objects. The same argument applied to the sub-walk ending at old's length shows chain[old.leaf_count - 1] == old.tip is exactly equivalent to chain.starts_with(&old.chain). So the byte-for-byte chain comparisons were always redundant with a single hash comparison against a value (old.tip) that's already a plain header field — old.chain itself was never actually needed.

    New read_current_descriptor (replacing read_current_chain/PriorChain) reads only that header, via a new read_prefix helper bounded to DESCRIPTOR_HEADER_MAX_LEN (143 fixed bytes + the largest possible ref-name length) — one small bounded read regardless of chain length, instead of reading and linearly checksumming the whole file. parse_descriptor_header is factored out of decode_descriptor_and_chain (unchanged for its own callers — AncestrySnapshot::decode/load, which do need the real chain and a working MMB) to parse just that shared prefix. This narrows the same class of guarantee read_current_chain already narrowed, one layer further: no checksum at all is verified on this path now, not just the root rebuild-and-compare. Independently re-reviewed field-by-field: repository/full_ref/tip corruption only ever misses a legitimate fast-forward/no-op (fails closed, safe); root is parsed but never read anywhere in advance; leaf_count only indexes chain via .get, never panics on an out-of-range value; generation is the one field reused directly, but it's independently cross-checked against the separately-read current pointer file before advance ever sees it. No corrupted header field can make advance persist a chain, root, or generation that the mandatory store-verified walk didn't itself produce.

    New tests: read_current_descriptor_matches_full_snapshot_fields (agrees field-for-field with the full decode path across several chain lengths, including the leaf_count == 1 edge case), read_current_descriptor_handles_a_long_branch_name (exercises the bounded-prefix read past the fixed header), and many_sequential_publishes_keep_one_generation_until_a_real_rewrite (asserts generation continuity at every step of 40 sequential fast-forwards, not just once at the end — a weaker end-to-end-only check would pass even with an off-by-one in the index, since two independently-minted random generations are virtually certain to differ regardless of whether the fast-forwards in between were handled correctly; confirmed by deliberately introducing that exact off-by-one during review and watching this test — and only this test, of the whole suite — catch it). All 803 mkit-core tests plus mkit-cli's history_mmr_*/reflog_and_amend integration tests pass.

    Isolating this mechanism from the fsync noise that swamps it in the full publish pipeline (a new #[ignore]d manual profiling test, profile_read_current_descriptor_vs_full_chain_read, not run in CI): at 50,000 leaves, reading the previous snapshot dropped from 735µs to 24.5µs per call — a 30x reduction (release build; debug showed 193x, 5.2ms → 27µs). cargo bench's sequential_publish series couldn't show this end-to-end: it's fixed at ≤300 leaves (where the old.chain read is microseconds either way, dwarfed by ~5-6ms of fsync per publish) and reaching an N where this cost would compete with that fixed fsync overhead takes far longer to benchmark than is practical here. SemVer: none — read_current_descriptor, read_prefix, parse_descriptor_header, and DESCRIPTOR_HEADER_MAX_LEN are private to history::ancestry; read_current_chain/PriorChain (both already private, added in the previous commit) are removed.

  • (core) stop rebuilding an ancestry MMR one leaf at a time, and stop building one at all when advance only needs the previous chain for comparison. Two follow-ups to the double-rebuild fix below:

    • CommitHistory gained extend, which merkleizes a whole chain in one new_batch/merkleize/apply_batch cycle instead of one cycle per leaf (each cycle allocates a MerkleizedBatch Arc, an overwrites map, and per-height dirty-bucket Vecs — real, if modest, per-leaf overhead for something the batch API is explicitly designed to do in bulk; verified against commonware-storage's own equivalent "N adds in one batch vs. N single-item batches" tests during review). AncestrySnapshot::build (used by every publish, and by decode/load) now calls it once instead of looping append. New mem_extend_matches_sequential_append_from_empty/_onto_existing_history tests pin that batching produces a bit-identical root/leaf-count to the same hashes appended one at a time, at sizes on either side of MMR peak-merge boundaries. No change to what gets verified against store — purely fewer allocations for the same tree.
    • advance's read_current call loaded the previous publish's full AncestrySnapshot — decoding its wire bytes and rebuilding its MMR from scratch — purely to read descriptor/chain for the no-op/fast-forward/generation-reuse checks; old.mmr was never touched. A new read_current_chain (backed by decode_descriptor_and_chain, factored out of AncestrySnapshot::decode) parses the same wire bytes into just the descriptor and chain, skipping the MMR rebuild. This one is a narrower, explicitly-scoped trade rather than a pure freebie: it also skips decode's rebuild-and-compare of root against the wire-persisted value, so a checksum-valid but internally-inconsistent snapshot file (reachable today only via a bug, or by anyone with the same local write access that already lets them edit any other mkit state directly — the checksum is a plain self-computed digest, not a MAC) now fails open on this path (a harmless spurious cache-miss/full rebuild) instead of decode's fail-closed error. Reviewed and judged safe because advance never persists old's data as-is: every use of it is a comparison against first_parent_chain(store, target), a fresh store-re-verified walk, and only that freshly-verified chain is ever written into the new snapshot; AncestrySnapshot::load and finish's own rebuild path are untouched and keep the full check. AncestrySnapshot::load (which does need a working MMR, for prove) keeps the full decode.

    Both are correctness-tested (800 mkit-core tests, including the two new ones, plus the history_mmr_records_commits/history_mmr_branch_lifecycle mkit-cli integration tests) but showed no statistically significant wall-clock change on cargo bench -p mkit-benches --bench history_mmr --features history-mmr even at a 40-sample run (publish/50/publish/250 are dominated by this suite's ~10 fsync/sync_dir calls per publish, which this sandbox's I/O jitter swamps any CPU-level saving in) — kept anyway since they're strictly-less-work with essentially no practical downside, and matter more on faster storage or larger histories where the fsync floor is smaller relative to chain length. SemVer: additive — mkit-core gains history::CommitHistory::extend; append's behavior is unchanged (now implemented in terms of extend). read_current_chain/decode_descriptor_and_chain/PriorChain are private to history::ancestry.

  • (core) stop rebuilding a branch's entire first-parent ancestry MMR twice on every history-mmr publish. history::ancestry::advance (refs::update_ref_with_ancestry, gated behind the history-mmr feature) used to call first_parent_chain(store, target) — one store.read_object + deserialize per commit from target all the way back to the repository's first commit — and build a full in-memory MMR from the result just to validate the target before persisting the publish intent, then unconditionally discard that snapshot and have finish redo the identical walk and MMR build a second time to actually write it. advance now keeps the snapshot it already built and hands it to finish (finish takes a new prebuilt: Option<AncestrySnapshot>, trusted only when its repository/full_ref/generation/tip exactly match the pending Transaction; crash recovery via recover, which only ever has a Transaction read back from disk, keeps passing None and rebuilding as before). cargo bench -p mkit-benches --bench history_mmr --features history-mmr (10-sample criterion A/B, this branch's host): publish/50 18.8ms → 7.5ms (−60%, p=0.01), publish/250 20.9ms → 12.5ms (−40%, p<0.01). SemVer: none — finish is private to history::ancestry; no public API change.

    A second, more aggressive change was prototyped alongside this — a first_parent_chain_from fast path that, on a plain fast-forward publish, spliced the previous publish's already-verified chain onto a short newly-walked suffix instead of re-walking target's entire ancestry from store every time (the mkit commit steady state would have gone from O(history depth) to O(new commits) per publish; a new history_mmr/sequential_publish bench added to measure it is kept). It was reverted before landing: a correctness review found it stopped re-verifying the reused prefix against store on every publish, silently narrowing the "validate the target from verified objects before persisting intent" guarantee advance's own doc comment asserts — a chain whose objects had been locally corrupted or partially GC'd out from under a stale snapshot would no longer be caught at publish time on the fast path (still caught, but only later, the next time anything calls AncestrySnapshot::load, which does its own independent full re-walk). Trading that integrity check for speed is not this change's call to make silently, so it's left as a documented follow-up rather than shipped.

  • (core) eliminate per-chunk allocate-and-copy in the streaming FastCDC reader. ChunkReader (the only chunker used for files above worktree::CHUNK_THRESHOLD — mkit add, and status/diff on any stat-mismatched large file) copied every byte twice per chunk: fill read into a separate 64 KiB scratch buffer and extend_from_sliced it into a growing window, then next_chunk cut the window with Vec::split_off (a fresh allocation plus a memcpy of the remainder) and mem::replace. It now reads into a single fixed 4 * max_size (1 MiB) buffer with start/end cursors advanced in place, compacting with copy_within only once the tail runs out of room (amortized over several max_size windows, not every chunk); a new next_chunk_ref returns a borrowed &[u8] for callers that don't need to own the bytes, and worktree::store_large_file_streaming uses it to avoid a further per-chunk copy. Chunk boundaries are unchanged (same FastCdc::cut over the same bytes) — the existing streaming_matches_in_memory_*/proptest_streaming_matches_in_memory tests all ran on inputs under the 1 MiB window and so never actually exercised fill's copy_within compaction path; a new streaming_matches_in_memory_iterator_across_multiple_compactions test (6 MiB, forcing several compactions) closes that gap, checking both next_chunk and next_chunk_ref output against ChunkIterator. New cargo bench -p mkit-benches --bench chunker_streaming suite (in-memory Cursor source, windowing cost isolated from BLAKE3/store I/O, 10-sample criterion A/B, this branch's 4-core host): 8 MiB 5.14ms → 4.63ms (−8.99%, criterion-reported CI [−10.4%, −7.6%], p<0.01), 32 MiB 22.14ms → 18.02ms (−16.7%, criterion-reported CI [−18.9%, −14.6%], p<0.01). SemVer: additive — mkit-core::chunker gains ChunkReader::next_chunk_ref; ChunkReader::new/next_chunk's signatures and behavior are unchanged. Minor version bump, not breaking.
  • (core,cli) parallelize push-plan delta encoding across rayon. transfer::plan_pack used to store.read + delta::encode (disk read + block-hash-table build + greedy scan, all sequential) once per delta candidate while planning a push; transfer::plan_pack now delegates that loop to a new transfer::plan_pack_with(..., encode_deltas) batch callback, and mkit-cli's push path passes a rayon fan-out (encode_delta_candidates_batch, mirroring the existing pack-compression/signature-verification fan-outs) instead of the built-in sequential one. cargo bench -p mkit-benches --bench delta_plan_fanout (new suite, isolating delta::encode over synthetic 64 KiB near-duplicate chunks, 4-core host): 256 candidates 46.99ms → 14.79ms (3.18x); the crossover is essentially immediate (each candidate already costs ~0.17ms, well above rayon's dispatch overhead), so pushes touching more than a handful of changed blobs benefit. SemVer: additive — mkit-core gains new public items (transfer::plan_pack_with, transfer::DeltaCandidate, transfer::encode_delta_candidate); transfer::plan_pack's signature and behavior are unchanged. Minor version bump, not breaking.
  • (core,cli) parallelize a single large file's chunk hashing across rayon. Every existing add/push/fetch fan-out (worktree hashing, pack compression, signature verification, delta encoding) parallelizes across independent files or objects, which does nothing for a worktree with one huge file — exactly the "Add + Commit One 1 GiB File" shape on the performance page. worktree::store_large_file_streaming_with (the new optional batched path) buffers up to 64 FastCDC-cut chunks per batch and hands each batch to a caller-supplied hash_chunks callback instead of hashing chunks one at a time; hash_file_with_metadata_with exposes that callback for file ingest; hash_file_with_metadata retains the borrowed sequential path. mkit-cli's add path (commands::add::hash_pending) passes a rayon fan-out once a batch is large enough to amortize dispatch cost (4 chunks/thread, mirroring the existing hash_fanout_threshold shape) — but only when add's own per-file fan-out (hash_pending_batch) is NOT already running in parallel: nesting a second fan-out into an already-saturated rayon pool was found (during review) to be unvalidated under worker contention, not the assumed clean scaling the original single-file benchmark showed against an idle pool, so hash_pending_batch's par_iter branch now hashes each file's chunks sequentially instead. The original cargo bench -p mkit-benches --bench chunk_hash_fanout measurements (4-core host, before combining with the reader optimization above) found that per-batch crossover is a wash at 8 chunks (0.641ms vs 0.646ms) but wins clearly from 16 chunks (1.477ms → 1.256ms, ~15%) through a full 64-chunk batch (9.774ms → 8.164ms, ~16%); end-to-end streaming ingest of one real file (hash_file_with_metadata vs _with's rayon path) is 8 MiB 107.2ms → 48.4ms (2.2x), 32 MiB 390.1ms → 230.6ms (1.7x), 128 MiB 2095.7ms → 1106.9ms (1.9x). SemVer: additive — mkit-core gains new public items (worktree::store_large_file_streaming_with, worktree::hash_file_with_metadata_with, worktree::store_chunk_blob); worktree::hash_file_with_metadata's signature and behavior are unchanged. Minor version bump, not breaking.
  • (cli) remote_dispatch::packmap::verify_new_object_signatures's parallel path (post-fetch Ed25519 signature verification, issue #692) now processes a 512-entry chunk cap decoupled from the small, pool-size-scaled crossover threshold that decides whether to parallelize at all — previously both used the same value (as few as 2 * num_threads, e.g. 16 on an 8-core host), so a large legitimate fetch (thousands of newly-signed objects) paid one rayon dispatch per ~16 entries (roughly 625 dispatches for 10,000 objects) purely to bound how much extra work a hostile fetch's first bad signature could force. The bound itself only needs to be small relative to an unbounded fetch, not tiny relative to core count: 512 keeps a hostile remote's forced extra work small and fixed while cutting the legitimate-fetch case to ~20 dispatches. SemVer: none — internal constant/behavior change, no public API change.
  • (core,cli) mkit-cli's parallel delta-encoding fan-out (encode_delta_candidates_batch, added above) now caches repeated base objects within batches of at most 64 candidates and a 64 MiB cache budget, with per-candidate reads for singleton or nonfitting bases — several chunks of one file commonly diff against the same prior chunk, and fanning that out across rayon without deduping turned what used to be redundant-but-serialized reads into concurrent redundant reads and concurrent redundant in-memory copies of the same bytes. New transfer::encode_delta_candidate_with_base (mkit-core) is the per-candidate unit that takes already-read base bytes instead of reading base itself; transfer::encode_delta_candidate is unchanged for the sequential default's benefit. SemVer: additive — new public transfer::encode_delta_candidate_with_base; existing behavior unchanged.
  • (core) stop decoding a Tree/ChunkedBlob twice on every ObjectStore::read_object call. read_object used to call Self::read (which, for a merkelized type, already deserializes the bytes once to recompute the BMT-root id for the integrity check) and then call serialize::deserialize a second time on the same bytes to produce the Object it returns — two full tree/chunked-blob parses for every read, on one of the hottest paths in the codebase (read_object is the primitive every tree-walker, diff, blame, merge, rebase, restore, stash, bisect, and graph op reads commits and trees through). New object::verified_id_and_object decodes once and hands back both the verified id and the decoded Object (None for a byte-hashed type, or when the merkle-decode itself failed — the same fallback-to-hash(bytes) object_id_from_bytes already had, now shared by both); read_object reuses the decoded object directly instead of decoding again, and object_id_from_bytes is now a one-line wrapper around the same function so the two callers can never drift. Alongside it, serialize::read_tree stopped cloning every entry's name into a separate prev: Option<Vec<u8>> purely to compare it against the next entry's name for the ordering check — the just-pushed TreeEntry already owns that name, so entries.last() gives the same comparison for free.

    Caught in review: the inherent-method fix alone missed the actual hot path. store::ObjectSource (the trait diff::load_tree, worktree::blob::LoadedBlob::load, and every other S: ObjectSource + ?Sized-generic caller actually call through) has its own default read_object (deserialize(self.read(h)?)) — the exact same double-decode pattern — and impl ObjectSource for ObjectStore only overrode read/read_unverified, not read_object, so Rust's static trait dispatch kept resolving every one of those generic call sites to the unoptimized default even though the concrete type was always ObjectStore. impl ObjectSource for ObjectStore now also overrides read_object to delegate to the fixed inherent method, so the fan-out of callers that motivated this change in the first place actually get it. Also, read/read_object's two independent (and, after the first pass, textually duplicated) StoreError::HashMismatch { expected: to_hex(..), actual: to_hex(..) } constructions are now one shared private check_hash helper. New tests pin both fixes directly: read_object_deserialises_tree/read_object_detects_tree_corruption_via_store_and_trait call read_object through &dyn ObjectSource specifically (the same static-dispatch shape load_tree/LoadedBlob::load use) and assert it matches the inherent method on both the happy path and a corrupted-tree HashMismatch.

    A second, broader review round (8 parallel angles against the full origin/main...HEAD diff) found one more sibling gap, independently flagged twice: EphemeralSink (the in-memory overlay diff/status read staged trees through) implements ObjectSource too but never overrode read_object either, so its store-fall-through path (a miss in the private map) also fell back to the trait's double-decoding default. impl ObjectSource for EphemeralSink now overrides read_object the same way: an overlay hit decodes the already-trusted bytes once directly, a miss delegates to self.store.read_object, reusing the now-fixed store-side decode. New ephemeral_sink_read_object_tree_overlay_and_fallthrough test covers both paths plus the store-fall-through corruption case, through &dyn ObjectSource. The rest of that review round's findings were all on files outside this diff (history/ancestry.rs, sign.rs, remote_dispatch/packmap.rs — pre-existing code from already-merged PRs) and are out of scope here; this diff's own code was independently confirmed by two review angles to reproduce prior behavior exactly, backed by direct regression tests.

    New cargo bench -p mkit-benches --bench object_read suite (release, criterion A/B, this branch's host): deserialize (in-memory decode only, no store) 100 entries 6.98µs → 5.58µs (−20.0%), 1000 entries 64.9µs → 52.9µs (−18.4%); read_object (through the store: read + id-verify + decode) 100 entries 204.3µs → 131.0µs (−35.9%), 1000 entries 796.0µs → 573.3µs (−28.0%) — measured against the inherent method directly, so these numbers were unaffected by the trait-dispatch gaps above; they now also apply to calls made through ObjectSource, on both ObjectStore and EphemeralSink. All 790 mkit-core tests pass, including read_detects_corruption/read_rejects_oversize_on_disk/rejects_unsorted_tree_entries, which pin that a corrupt or out-of-order tree is still rejected exactly as before. SemVer: none — verified_id_and_object and check_hash are private; ObjectStore's, EphemeralSink's, and ObjectSource's public signatures are unchanged, and both impl ObjectSource blocks gaining a read_object override changes behavior (faster, same results) but not the trait's documented contract.

  • (core,cli) parallelize a ChunkedBlob's per-chunk read fan-out during checkout/clone/reset/restore — the read-side counterpart of the ingest-side chunk-hashing fan-out add already had. ops::restore::restore_blob's ChunkedBlob arm read every chunk (open + BLAKE3-verify + decode, via ObjectStore::read_object) sequentially, one at a time, even though each chunk's read is independent of every other chunk in the same file — the exact "add's ingest side is 4-core, checkout's read side is 1-core" gap chunk_hash_fanout's own doc already names for the write direction. New ops::restore::restore_tree_to_worktree_with takes an explicit read_chunks batch callback (mirroring worktree::store_large_file_streaming_with's hash_chunks shape, for the same reason: mkit-core has no thread-pool dependency of its own, so it stays usable from wasm targets) in place of a built-in sequential loop; chunks are read in bounded batches of RESTORE_CHUNK_BATCH (64, matching worktree::STREAM_HASH_BATCH's 16 MiB window) instead of one at a time, and a batch of raw bytes shorter or longer than its input is a new typed RestoreError::ChunkBatchLengthMismatch rather than a silent desync. restore_tree_to_worktree (unchanged signature) is now a thin wrapper passing a sequential default. mkit-cli's new restore_fanout::read_chunks_fanout wires a rayon fan-out into every checkout/clone/reset/restore/sparse-checkout call site (commands::{checkout, clone, reset, restore, sparse_checkout} and commands::restore_worktree_and_index), using the same "sequential below a threshold, par_iter at or above it" crossover shape as commands::add's own chunk-hashing fan-out (crate::fanout::try_map_seq_or_par).

    New cargo bench -p mkit-benches --bench restore_chunk_fanout suite (4-core host): batch/N_chunks (isolated per-batch crossover, sequential vs rayon) — 8 chunks is a wash (0.164ms vs 0.170ms), 16 chunks already wins clearly (0.343ms vs 0.265ms, ~23%), 64 chunks (a full batch) 1.441ms vs 0.638ms (~56%); file/N_mib (end-to-end restore of one real FastCDC-chunked file, sequential restore_tree_to_worktree vs fully rayon-fanned restore_tree_to_worktree_with) — 8 MiB 5.893ms → 4.745ms (~19%), 32 MiB 26.458ms → 21.042ms (~20%), 128 MiB 115.293ms → 83.199ms (~28%). The end-to-end win is smaller than the isolated batch win because a restored chunk's cost also includes a sequential write_all into one shared tmp file (never fanned out — file writes must stay in chunk order), so parallelism only covers the read/verify/decode share of each chunk. All existing ops::restore unit tests (including restore_chunked_blob_reassembled/restore_rejects_chunked_total_size_mismatch) and mkit-cli's checkout/clone/reset/restore/sparse-checkout integration tests pass unchanged; cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --check are clean. SemVer: additive — mkit-core gains ops::restore::restore_tree_to_worktree_with and RestoreError::ChunkBatchLengthMismatch (a new variant in a public exhaustive error enum can require downstream match updates); restore_tree_to_worktree's signature and behavior are unchanged.

  • (core) mkit-core::merkle's BMT inclusion-proof construction is now byte-identical to commonware_storage::bmt::Proof at the pinned 2026.9.0 train, replacing the prior provisional format. Proof is a new public struct (leaf_count: u32, siblings: Vec<Hash>) encoded as u32 BE leaf_count ‖ varint(n) ‖ n × 32-byte digest via commonware-codec, decoded through Proof::decode(bytes, max_items) with allocation bounded to max_items * MAX_LEVELS (MAX_LEVELS = 32) before any sibling is read. Sibling selection is ported from upstream: level-major bottom-up, index-ascending, omitting a sibling that would be a node's own odd-trailing duplicate or is already covered by another proven position in the same proof — the prior format always emitted the self-duplicate. Range (build_tree_entries_range_proof/build_chunks_range_proof) and multi-leaf (build_tree_entries_multi_proof/build_chunks_multi_proof) proofs are new. build_{chunk,tree_entry}_inclusion_proof/verify_{chunk,tree,}_inclusion_proof are replaced by build_{chunk,tree_entry}_proof and id-based verifiers — verify_tree_entry, verify_chunk, and their range/multi counterparts — which check against the object's id (via the new wrap_id(ObjectKind, inner_root) helper) rather than the bare pre-domain-wrap inner root the old API handed callers, closing an inner-root-vs-id confusion footgun (issue #1015 §Security); verify_chunk and its range/multi counterparts additionally reject position 0 (the ChunkedBlob metadata leaf) outright. A new native-only cross-check test (merkle::tests::proofs_match_commonware) proves byte-identity and mutual verifier acceptance against commonware_storage::bmt across randomised trees and single/range/multi positions; golden vectors live under rust/tests/golden/proofs/ (SPEC-MERKLE-OBJECTS §5.6). No change to any object id, signing byte, or existing golden vector — only the proof bytes and API move. SemVer: breaking — mkit-core::merkle's proof-related public API (types and function signatures/bytes) changed; compute_tree_id/compute_chunked_id and every other object-identity function are unchanged. Sanctioned by SPEC-MERKLE-OBJECTS §5's now-removed provisional carve-out and CONTRIBUTING's pre-production policy; the crate version bump itself happens in the release-prep PR, not here.
  • (core) SPEC-HISTORY-PROOF's ancestry primitive switches from a Merkle Mountain Range (MMR) to a Merkle Mountain Belt (MMB) — commonware-storage's newer structure (https://arxiv.org/abs/2511.13582), already available in the exact pinned version (=2026.9.0) mkit depends on, so this needed no dependency bump. An MMR requires strictly decreasing peak heights, so a single append can cascade up to O(log N) internal-node merges whenever a run of same-height peaks collapses at once (e.g. crossing a power-of-two leaf count); an MMB allows up to two same-height peaks and merges at most one pair per append, bounding the worst case to a constant instead of only the amortized average — the property that matters for mkit commit's one-append-per-publish pattern. CommitHistory (mkit-core::history) now wraps commonware_storage::merkle::mmb::mem::Mmb instead of merkle::mmr::mem::Mmr; both are the same generic Mem<Family, Digest> machinery (Clone, batch API, pruning, proof verification) with only the Family (peak/position topology) differing, so the swap was a mechanical type-and-terminology change confined to history.rs — ancestry.rs and everywhere else in the codebase only ever used mkit's own Position/InclusionProof/CommitHistory wrappers, never MMR types directly. HistoryError::Mmr is renamed HistoryError::Mmb; the history-mmr Cargo feature and .mkit/history* paths keep their names (identifiers, not a structural claim) per SPEC-HISTORY-PROOF's updated preamble.

    This changes on-disk roots and InclusionProof bytes for the same commit sequence (different forest topology), and pre-production has no back-compat requirement for it (confirmed with the maintainer) — no version bump, dual-format support, or migration path, just an in-place spec rewrite. SPEC-HISTORY-PROOF §§1–5 are updated throughout (root/proof formulas, the digests peak-iteration order, which is now non-increasing rather than strictly decreasing since MMB peaks can tie); mkit also does not adopt the same paper's "Pyramid" P-MMB bagging variant, since it is not yet implemented in the pinned commonware release (mkit keeps Bagging::ForwardFold, matching what F-MMB uses).

    cargo test -p mkit-core --features history-mmr (800 tests, including two pre-existing MMR-generic property tests that now exercise MMB unchanged) and mkit-cli's history_mmr_*/reflog_and_amend integration tests all pass unmodified — the swap changes proof/root bytes, not any documented behavior. Measured effect: cargo bench -p mkit-benches --bench history_mmr --features history-mmr's publish/load series (through the full fsync-heavy durable-publish pipeline) show no measurable difference — expected, since total node count differs from MMR by under 2% at these sizes and shrinks below 0.1% by a few thousand commits (computed directly: N=250, MMR 494 nodes vs MMB 493; N=10,000, 19,995 vs 19,987), so it was never going to show through multi-millisecond fsync noise. A new in-memory-only bench_in_memory_build (batched extend over the whole chain, no I/O) shows a small, consistent ~5–8% improvement across N=50..5000, matching that same node-count delta. A separate attempt to isolate the actual claimed property — worst-case single-append latency over a sequence of individual appends — was prototyped but removed: allocator/scheduler noise in this sandbox dominated the microsecond-scale effect being measured and produced a directionally contradictory result at one size, so it would have been misleading to ship. The structural claim itself (node-count formulas, bounded merge-per-append, and the proof-size bound for the most recent leaf) is verified directly against commonware-storage's own source and test suite (mmb::proof::test_last_element_proof_size_is_two), not re-derived here. SemVer: breaking for any consumer that persisted ancestry snapshots or InclusionProof bytes under the prior MMR-based format, or matched on HistoryError::Mmr; the feature is opt-in (history-mmr) and the project has no production deployments of it.

  • Sparse proofs and caches move to v2 canonical Tree witnesses. Verification requires the independently requested Tree ID and derives the exact selection; recursive witnesses establish completeness. Caches use .witness files; verification returns the derived entries directly.
  • The staged index accepts only checksummed v3. Unsupported or corrupt indexes fail safely, including before GC; there is no migration or automatic rebuild.
  • History proofs now describe canonical first-parent ancestry in explicit generations, with locally trusted contextual descriptors and recoverable publication. Bounded complete snapshots use O(chain length) reconstruction; obsolete journal backends and their runtime dependencies are removed.
  • The keys Worker stores authoritative names and replay records in per-key SQLite Durable Objects, with no KV fallback. Repository Workers use the shared transactional replay ledger without obsolete idempotency tables.
  • (cli, internal) mkit-cli's six "sequential below a per-thread threshold, rayon par_iter at or above it" fan-outs (commands::add's per-file and per-chunk hashing, remote_dispatch's pack-compression and delta-encoding, remote_dispatch::packmap's signature verification) each hand-duplicated the branch-and-collect boilerplate around the shared fanout::threshold formula. New fanout::map_seq_or_par/fanout::try_map_seq_or_par factor that shape out for the four call sites it fits exactly (by-reference, in-order, infallible or Result-collecting); prepare_delta_batch (consumes by value) and verify_new_object_signatures (deliberately chunks its parallel path — see the entry above) keep their own loops since forcing either into the shared shape would need extra generic machinery to claw back what a bespoke loop gets for free. No behavior change. SemVer: none — pub(crate)-only, no public API surface.
  • bump the commonware crate family from the 2026.7.1 release train to 2026.9.0 across every Rust workspace (rust/, contrib/signers/, apps/repo-worker, apps/vcs-worker) (MKIT-2). Notable upstream changes this required adapting to:
    • merkle::full::Merkle::{apply_batch, sync} now take self by value; CommitHistory's journaled backend handles a failed mutation by leaving the handle poisoned (HistoryError::Poisoned) instead of panicking, and root()/len() fall back to the last known-good value rather than requiring a Result.
    • commonware-runtime 2026.9.0 added a per-storage-directory advisory .hold file lock, which deadlocked opening a second branch's history journal in the same process; CommitHistory now shares one bootstrapped Context per <mkit_dir>/history directory across every branch opened in-process (see docs/INVARIANTS.md, "One commonware storage Context per history dir per process").
    • threshold::recover dropped its Faults generic and sharing::Mode lost its Default impl; mkit-attest's BLS threshold signer names Mode::NonZeroCounter explicitly (same value the old default resolved to — pinned by a new golden-vector test).
    • commonware_parallel::Strategy can no longer be implemented outside the commonware-parallel crate (Manual::new was removed); pack_shard.rs's spy-based CountingStrategy test was removed accordingly (see docs/INVARIANTS.md).
    • blst is now opt-in upstream (only pulled in via commonware-cryptography's std → bls12381 feature chain), not unconditional as the 2026.7.0-era comments said.
    • Windows: see the Removed entry above — this is what motivated dropping Windows as a supported target.

Deprecated

  • mkit-transport-enc is deprecated: there is no maintained server for it. Use SSH (mkit serve) or Connect.

Removed

  • mkit serve --http and mkit serve --listen-enc. mkit serve <PATH> is now only the mkit+ssh:// forced-command server (SSH-frame protocol on stdin/stdout, unchanged). Its HTTP and encrypted listeners are gone, so the CLI carries no HTTP server stack or SQLite. The removed flags are clap usage errors (exit 64). Removed with them: --http-token, --unsafe-allow-any-http-peer, --enc-authorized-peers, --enc-server-key, --unsafe-allow-any-enc-peer, --enc-idle-timeout-secs, --enc-handshake-timeout-secs, and mkit-cli's http-transport cargo feature (enc-transport stays: it is the deprecated mkit+enc:// client). The pre-production policy allows the removal without a deprecation period.
  • Standalone server binary and container image. There is no mkit-server binary, release archive or ghcr.io container image, and no native SQLite/S3/filesystem server deployment. Serve repositories over SSH with mkit serve, or on Cloudflare Workers with the mkit-server-worker adapter. The mkit-server library crate and the Workers adapter remain.
  • mkit-transport-connect's server cargo feature (breaking). The axum-hosted server that backed mkit serve --http is gone, with its public API: serve, router, TransportServer and map_transport_error, and the axum and connectrpc-health dependencies. The crate is now the mkit+https:// client (ConnectTransport) only; serve mkit.transport.v1 with the mkit-server library or its Workers adapter. Removing a published feature and public items is semver-breaking, so the next release of mkit-transport-connect (and, with lockstep versioning, every mkit-* crate) is 0.5.0, done at the release that merges the mkit-server work to main (cargo-semver-checks runs at the tag, crates-publish.yml). crates.io lists mkit-cli as its only published dependent, and no published mkit-cli enables the feature by default. mkit-server-conformance's legacy mkit serve --http wire baseline is removed with it.
  • Compatibility-only index readers/migration APIs, legacy history APIs, the hash-only rename API, and redundant sparse-selection APIs. Pre-production development supports the current formats without backward-compatibility shims.
  • Windows is no longer a supported build, test, or release target (MKIT-6). commonware-runtime 2026.9.0's non-Linux storage-sync path calls libc::sync(), which does not exist on x86_64-pc-windows-msvc — the workspace and its test suite (not just the default-feature mkit binary) no longer build there. Rather than ship a Windows CI/release leg that cannot actually build or test the workspace, or maintain an untested Windows-only subset, Windows was dropped: the windows-smoke CI job, the x86_64-pc-windows-msvc release leg (and its .zip archive), the install.ps1 installer, the Scoop packaging manifest and release-verify.yml's Scoop channel check, and the windows-credential leg of the keystore-backends CI matrix are all gone. mkit-keystore's backend-windows-credential feature and its BackendKind::WindowsCredentialManager variant were removed — "windows-credential" is now an unrecognized backend name (key backend: ... / exit CONFIG_ERROR) rather than a compiled-in-but-platform-unavailable one (previously exit UNAVAILABLE, "requires Windows"). Removing a public enum variant from a published crate is semver-breaking: mkit-keystore (and, per the workspace's lockstep versioning, every mkit-* crate) needs a 0.5.0 release, not 0.4.3, to ship this. Windows users should run mkit under WSL, which uses the Linux binary — see docs/INVARIANTS.md's "Windows is not a build, test, or release target" entry.

Fixed

  • (core) Pack writer no longer zstd-compresses a payload over MAX_RAW_OBJECT_SIZE, which readers reject.
  • (core) Pack framing no longer overflows a 32-bit usize: a payload_len near u32::MAX made pos + payload_len trap on wasm32 (release builds keep overflow-checks) in PackEntries::new, delta_base_hashes and, through them, verify_closure_packs (exported by mkit-wasm). It is now PackError::UnexpectedEof; a wasm32 test in mkit-core-wasm-check pins it.
  • (core) The closure BFS queues each id once. A tree whose entries all name one object no longer grows the queue per reference; fetch order is unchanged.
  • (core) The disclosure builder no longer panics on a ChunkedBlob range whose offset + len overflows u64 (e.g. mkit prove --range 10:18446744073709551615) or on a chunk shorter than its 10-byte Blob prologue: these now return VerifyError::OffsetOverflow and VerifyError::Decode(UnexpectedEof). Valid bundles are byte-identical.
  • (core) A Range payload over a chunked leaf (chunk = Some(hdr)) now rejects len == 0 before running the chunk header's wrap/fold checks, matching SPEC-DISCLOSURE §4's stated order — the bundle is rejected either way, only the specific typed error changes. New golden negatives neg_zero_length_range_chunked and neg_range_chunk_meta_forged_total_size. SemVer: none.
  • (core) verify::resolve_absolute_offset's final sum + offset_in_blob addition now uses checked_add, matching every neighbouring offset computation, and returns VerifyError::OffsetOverflow on overflow instead of wrapping. SemVer: none — the affected inputs are far outside anything a conformant bundle can produce.
  • (cli) mkit verify-proof <commit-id> - now caps the stdin read at MAX_BUNDLE_BYTES instead of buffering an unbounded stream before the size check ran, rejecting an oversized bundle with DATAERR instead of blocking or exhausting memory on an adversarial or unbounded input. SemVer: none.
  • (cli) mkit_verify_proof's MCP tool descriptor no longer claims readOnlyHint = true — its payload_out field writes a file, so it now matches mkit_prove's hints (not read-only, not destructive, idempotent). SemVer: none (annotation-only).
  • (workspace-worker) authenticate now drains the request body before any envelope-header check can reject it, instead of after. An unauthenticated (or otherwise malformed-envelope) POST with a body of roughly 200 KiB or more previously got its rejection response written while the client was still writing that body, which the local Miniflare/workerd HTTP stack surfaces as a connection reset (ECONNRESET) instead of delivering the intended 401/403/415 — seen as an intermittent workspace.integration.test.ts failure. SemVer: none — worker-only, no public API.
  • (core) verify_closure / verify_closure_packs merge the walker's corrupt list with index-time deserialize failures instead of overwriting it. The walker list is empty on these paths today (both indexes are keyed by derived id), but a fetch that returned bytes hashing to a different id would previously be dropped. SemVer: additive.
  • (core) cargo check -p mkit-core --no-default-features --target wasm32-unknown-unknown now compiles: on wasm32, getrandom 0.4 is enabled with the wasm_js feature (same posture as mkit-wasm), so the crate no longer fails the getrandom "unknown-unknown not supported by default" compile error. Native builds are unchanged. SemVer: none.
  • (cli) mkit closure verify (local, no --from) no longer exits with a hard read error the moment it hits one corrupt on-disk object — it now catches StoreError::HashMismatch per object, reports it under the ClosureReport's corrupt list (removing it from missing first, if the walk had already reached it), and continues, matching the command's documented fsck-shaped behavior. Also hides the note: N unreferenced list (and the JSON unreferenced array) in local mode unless a new --show-unreferenced flag is passed — the local store is expected to be a superset of any single commit, so the list was noise by default. New CLI test flips a byte in an object file and expects bad: closure incomplete: 0 missing, 1 corrupt. SemVer: additive (new flag; local-mode corrupt/unreferenced reporting changes for a case that previously hard-failed the whole command).
  • (core) verify::resolve_absolute_offset now rejects a non-empty chunk_len_proofs on a Range payload whose disclosed chunk is index 0, instead of silently ignoring it — index 0 has nothing preceding it to describe, so any entry there is meaningless, exactly like the existing rejection on a plain-Blob leaf (VerifyError::UnexpectedLengthProofs, now shared by both cases). New unit test len_proofs_on_chunk0_are_rejected and golden negative vector rust/tests/golden/disclosure/neg_len_proofs_on_chunk0.*; SPEC-DISCLOSURE §4 states the rule normatively. SemVer: additive — tightens verification; the only bundles affected are malformed ones no conformant builder has ever produced.
  • (core) advance's advisory write_scrub_state call (history-mmr) no longer fails the whole publish if the write itself fails — finish has already durably committed the ref move and ancestry snapshot by that point, so a caller must not see that succeeded operation reported as a failure; losing this purely-advisory bookkeeping write only costs the next publish some extra re-verification, never correctness. Found by code review alongside the ScrubState panic above; regression test forces the write to fail (a directory occupies the scrub file's path) and confirms the publish still succeeds.
  • (core) decide_chain's full-walk fallback (history-mmr, on a completed scrub lap or a stale re-verification schedule) no longer re-walks and re-reads the fast-forward suffix a second time — it splices the suffix already verified moments earlier onto a fresh walk of just the reused prefix, which first_parent_chain(store, target) == first_parent_chain(store, d.tip) ++ suffix makes provably identical to a full walk of the whole chain. New regression test plants corruption deep in the prefix and confirms the spliced fallback still catches it, proving the optimization didn't drop real verification along with the redundant re-read.
  • (cli) mkit fetch's tracking-ref snapshot (used to report which refs moved) now uses the same parallel list_remote_refs_parallel fan-out every other ref-listing call site already got when that fan-out was added — this one spot was missed.
  • (core) HistoryError is now #[non_exhaustive], matching GcRootsError's existing convention in the same crate — which commonware-storage structure backs the ancestry proof (the Mmb variant's own name is already the result of one such internal rename) is an implementation detail, not part of this error type's contract, and should not force a breaking change on downstream exhaustive matches the next time it changes.
  • (core) RefError is now #[non_exhaustive] too, for the same reason and found in the same code-review pass — it gained RefBatchLengthMismatch in this same set of changes with no such marker, which would have made that addition a breaking change for any downstream exhaustive match.
  • (core) decide_chain's two "no compatible previous generation to fast-forward from" branches (no prior publish at all, and a first-parent search that never reaches the previous tip) built an identical fresh ChainDecision::NewGeneration — full walk plus a fresh ScrubState — via separately hand-copied code; factored into one new_generation helper both branches call, found as duplication by code review. No behavior change.
  • (core) decode_descriptor_and_chain's minimum-payload-length check and DESCRIPTOR_HEADER_MAX_LEN each independently hard-coded the 143-byte fixed descriptor header size (as 175 — 143 + 32 for the trailing checksum — and 143, respectively), found as duplication by code review. Both now derive from one new DESCRIPTOR_HEADER_LEN constant. No behavior change.
  • (core) read_prefix (history-mmr's bounded prefix read, used by read_current_descriptor) now preallocates its buffer to the caller's known max_bytes bound instead of letting read_to_end grow it via generic doubling — found as a minor inefficiency by code review; max_bytes is small in every call this crate makes (currently DESCRIPTOR_HEADER_MAX_LEN, under 64 KiB), so the bound was always cheap to preallocate outright. No behavior change.
  • (cli) remote_dispatch::packmap::verify_slice (post-fetch signature verification's per-chunk read step) now drops each read object immediately unless it's a Commit/Remix/Tag, instead of retaining every object in a chunk — Blob/Tree/ChunkedBlob/Delta included — for the rest of the call even though neither the batch path (collect_batch_entries) nor the fallback (verify_one_object) ever inspects them. Found by code review: a single Blob can be up to worktree::CHUNK_THRESHOLD (1 MiB — mkit chunks anything larger), so a verify_chunk_size()-sized chunk (up to 512 entries) dominated by such blobs could previously hold up to ~512 MiB of fully decoded, never-inspected object bytes resident at once on a fetch a hostile or simply blob-heavy remote controls the shape of. New regression test interleaves unsigned Blob entries with signed (and, separately, tampered) commits and confirms verification still correctly accepts/rejects — the filtering doesn't drop or misattribute a neighboring entry. No behavior change to what's accepted or rejected, only what's held in memory while deciding.
  • Shared browser login across all seven web pages, with a seven-day HttpOnly session, separate in-memory signing unlock, session-scoped workspace queries, and a repeatable navigation/refresh/sign-out verification workflow.
  • (core) worktree::store_large_file_streaming_with's and transfer::plan_pack_with's hash_chunks/encode_deltas batch-callback cardinality contracts (exactly one result per input item) are now enforced with a typed error (WorktreeError::ChunkBatchLengthMismatch, StoreError::DeltaBatchLengthMismatch) instead of, respectively, silently trusting the result (store_large_file_streaming_with had no check at all) or panicking (plan_pack_with's prior assert_eq!) — a buggy or third-party batch callback now fails the operation cleanly rather than either corrupting a ChunkedBlob manifest or crashing the process mid-push. Chunk hashing contract failures return exit::SOFTWARE (70, sysexits EX_SOFTWARE); push retains its existing general-error exit code for delta callback failures. Callbacks remain responsible for preserving result order. SemVer: new variants in public exhaustive error enums can require downstream match updates; both functions' happy-path behavior is unchanged.
  • (cli) Closed a gap where mkit pull/mkit fetch (via remote_dispatch's per-branch fast-forward and unpack/publish locks) and mkit status's opportunistic stat-cache refresh took the worktree lock directly instead of through commands::acquire_worktree_lock, silently skipping the "this root is being served by mkit serve" warning that commit/checkout/gc already print in the same situation (SPEC-CONCURRENCY §3.1). commands::warn_if_served is now called at all four sites; new serve_guard.rs regression test (pull_warns_when_root_is_being_served).
  • Clarified docs-site copy, aligned controls and overlays with Pigment, added layout-matched loading placeholders and grouped navigation, and stabilized virtualized lobby scrolling across tab resume and row resizing. Removed the development Agentation integration. No public API or format changes.
  • Worker writes rejected by quotas or rate limits no longer allocate replay records, including chat posts and reactions. Existing operation retries still reuse their reservation or saved reply without a second quota charge.
  • Authenticated transport requests send the required envelope-version header. External signing negotiates the advertised raw-byte or opaque-handle key form, including the bundled CTAP signer.
  • Restaging a regular file as a symlink preserves a valid Blob target even when the old file used a chunked representation.
  • Ref lock filenames stay bounded for valid long and nested ref names.
  • HTTP and S3 shard downloads process successful responses while admitting workers, so an available quorum is not blocked by redundant slow requests.
  • Git correspondence audits now derive imported fields and graph edges from retained source bytes, verify pinned signatures and exact provenance claims, and work without the import private key.
  • File comparisons, restaging, merge shortcuts and exact rename detection now recognize equal content across valid Blob and ChunkedBlob representations. Content-comparison and source-aware snapshot APIs propagate malformed or missing chunk errors. Status uses the same content-aware rename detector.
  • Pack and packmap consumers reject bytes or shard manifests that do not match the requested content key before unpacking or publishing effects.
  • All local ref mutations participate in the same per-ref lock, including unconditional writes, deletion, tags and remote refs. File-transport writes serialize every condition through their separate transport lock.
  • External signers receive a signing request only after a separate compatible capabilities response is validated.
  • Signed transport writes use destination-bound auth v2, including the content commitment, repository, validity and stable nonce. Worker effects, quota and replay results commit atomically; immutable uploads can resume without a second quota charge. Repository config cannot enable ambient signing. Only the current auth v2 contract is supported.
  • File-fetch staging now bounds retained pack payload, disk usage and chain counts; shard downloads share process-wide memory and concurrency budgets.

Security

  • (core, security) Fix denial of service (DoS) in pack reading by bounding owned memory and checking framing arithmetic on 32-bit targets.
  • (core) Fixed a crash (slice index out of range panic) in history-mmr ancestry publish's bounded scrub-window verification, found by code review. ScrubState (the rolling re-verification schedule for a branch's reused ancestry prefix) carried no binding to the generation it was computed against; advance's advisory write_scrub_state call runs strictly after finish has already durably committed a publish, so a crash (or a failed write) in that window left a rewrite/reset's old generation's scrub state — sized for its own, possibly much longer, chain — on disk paired with the new, possibly much shorter, one. The next ordinary fast-forward would then compute a scrub window against the old verified_through and slice a chain far too short for it. ScrubState now records and validates the generation it was computed against; a mismatch (this exact crash window, or any other cause) is treated exactly like "no prior scrub state" and falls back to a full walk, the module's existing fail-safe design for missing or corrupt state. New regression test reproduces the exact on-disk byte state without needing to inject a crash mid-advance, confirmed to panic without the fix and pass with it. SemVer: none — ScrubState's on-disk format changed (magic bumped \x01 → \x02); a pre-upgrade file simply fails to decode under the new layout and falls back to a full walk, the same safe behavior a missing file already gets.
  • (core) Closed a second, independent way to hit verify_scrub_window's slice index out of range panic (above), found by a follow-up code review of that same fix: the generation binding rules out a stale scrub file (computed against a different, superseded generation), but not one whose verified_through simply exceeds the actual on-disk prefix length for a generation it genuinely does match — e.g. a snapshot file restored from an older backup paired with a newer scrub file. decide_chain now checks the scrub window's end bound against the real prefix length before calling verify_scrub_window, falling back to a full walk (the same fail-safe path a generation mismatch already takes) instead of handing it a range it can't satisfy; verify_scrub_window itself now also bounds-checks via slice::get and returns a typed HistoryError::Corrupted rather than a bare index, so a future caller with the same class of bug fails closed instead of panicking. New regression test constructs the exact mismatch directly (real production code cannot produce it, by the monotonic-append argument decide_chain's own docs make) and confirms it panics without the fix, falls back to a full walk with it. SemVer: none.
  • (core) read_scrub_state (history-mmr) no longer propagates an I/O error reading the advisory scrub file — found alongside the panic fixes above while auditing the same fail-safe contract from the read side. ScrubState's own docs promise "missing or corrupt scrub state is not an error", and write_scrub_state's caller already treats a write failure as advisory-only (let _ = write_scrub_state(...)), but read_scrub_state still used ? on read_bounded's result — so anything that made the file briefly unreadable (a stray directory at that path, exactly the state an existing test already constructs to exercise the write-side fix; a permission change; a file that grew past the 93-byte cap) turned every subsequent fast-forward publish on that branch into a hard failure instead of degrading the verification schedule. Now maps any read failure the same way a missing file already is: Ok(None), falling back to a full walk. New regression test publishes twice with a directory blocking the scrub path throughout and confirms the second publish — the first fast-forward that actually reaches read_scrub_state — no longer fails; the existing write-side test only ever exercises a first publish, where read_scrub_state isn't reached at all. SemVer: none.
  • (cli) mkit mcp --http <addr> now refuses to bind without authentication, matching mkit serve --http's fail-closed design. Previously it bound the given address (not restricted to loopback despite its own doc comment's claim) with no Authorization check at all — any network-reachable caller got unauthenticated access to the full MCP tool catalog, including mutating tools like mkit_checkout. It now requires a bearer token (--http-token <TOKEN> or the MKIT_MCP_TOKEN env var — a name of its own, not serve --http's MKIT_API_TOKEN, since the two surfaces have different threat models and must not share a secret) or an explicit --unsafe-allow-any-http-peer opt-out that prints a loud warning, enforced on every request via a new BearerAuthHttp tower middleware wrapped around StreamableHttpService. New mcp_v2_http.rs mod auth integration tests cover: refusal with no token/flag, refusal on an empty token, refusal when both a token and the unsafe flag are given, 401 on a missing/wrong Authorization header, success with the right token, and the MKIT_MCP_TOKEN env fallback. SemVer: additive — new CLI flags, new env var; existing --http usage without them now refuses to start rather than serving unauthenticated (a deliberate behavior change gated by the same version bump the removed-Windows-support entry below already requires).

0.4.2 - 2026-09-02

Performance

  • (cli) parallel worktree hashing for add -A/add . (#951)

Other

  • upgrade connectrpc 0.8.x to 0.9.0 and buffa 0.8.1 to 0.9.1 (#939)

0.4.1 - 2026-07-16

Bug Fixes

  • (core,cli) close a torn-read race between concurrent history-journal opens and writes (#873)

0.4.0 - 2026-07-16

Bug Fixes

  • (cli) move remote state through a temp sibling so prefix-nested renames work (#657) (#659)
  • (core,cli) close branch -m vs commit ref-loss race left open by #637 (#672)
  • (core,cli) wire MMR crash recovery and lock ordering (#555) (#580)
  • (core) index reader rejects nonzero object_hash on Removed entries (#557) (#576)
  • (core) enforce total_size on ChunkedBlob reassembly (#550) (#567)
  • (blame) close -C merge residual — interior first-parent carve-out + boundary all-parents search (#507)
  • (blame) trace -C copies across every merge parent (review #503)
  • (blame) address review — reverse range hardening + O(1) liveness exit
  • (blame) address review — Arc-share ignore_revs, ignore-rev wins over -M/-C
  • (blame) address review — diff_trees reuse, no tree-walk recursion, fewer clones
  • (blame) keep -w and implied -M when blaming a copy source
  • (blame) position-stable line matching; fast-fail -w size guard
  • (cli) address review round 10 — mv dir-batch, D/F continue, abort ancestor guard, stash disambiguation, +docs
  • (cli) address review round 9 — D/F conflict atomicity, empty-merge index, mv symlink guards, stash fail-closed, +docs
  • (cli) address review round 8 — skip/abort edge cases, mv dir-child integrity, stash unborn untracked, +docs
  • (cli) address review round 7 — abort edit-preservation, mv case/source/dest guards, stash unborn dirs, +docs
  • (cli) address review round 6 — continue/abort data loss, mv symlink/ancestor, stash unborn, +docs
  • (stash) preserve staged deletions in --index via a serialized index snapshot
  • (cli) address review round 3 — cherry-pick -n conflicts, branch --contains default, merge parent, stash index errors
  • (cli) address review round 2 — stash --index atomicity + config subsection case
  • (cli) address peer review — cherry-pick -m is mainline, config files case-insensitive
  • (rpc,attest) enforce SPEC-RPC §3.3/§4 receiver strictness (#558) (#581)
  • (serve,transport) surface CAS conflicts as RefConflict per SPEC-TRANSPORT §4.2.1 (#551) (#568)
  • (keystore) drop orphaned systemd_creds re-export (linux test build)
  • (keystore) qualify static_label in linux/windows backends
  • (transport-http) bound + validate advance_refs (PR #422 review)
  • (transport-s3) reject non-canonical ref bodies per SPEC-REFS §1 (#552) (#565)
  • (s3) use Self:: in intra-doc link; scrub stray finding ref in test
  • reformat two import lines to satisfy the CI-pinned rustfmt (#781)
  • (cli) thread ssh.* trust-pinning config into spawned ssh (#389) (#435)
  • (cli) preserve a nested sibling remote's refs and bridge state across prefix rename/remove (#660) (#789)
  • converge buffa/connectrpc across all 4 lockfiles on latest published versions (#805)
  • (cli) create destination parents when remote rename moves tracking refs and bridge state (#596) (#605)
  • (sparse) wire the bitmap-cache read path into checkout/clone (#556) (#579)
  • (serve,enc) cumulative per-connection frame/byte budgets on the enc listener (#559) (#578)
  • (cli) address review round 2 — checkout -b atomicity + color/tag fixes
  • (cli) address peer review of PR #402 — tag peeling, -c reach, --color
  • (cli) address PR review — fill parity gaps, document deferrals
  • (cli) address christopherwxyz review — merge no-op commit, abort atomicity, mv/config cleanups
  • (cli) address review round 11 — mv dir-dest symlink guard, atomic abort pre-flight, completion fixes
  • (cli) self-review follow-up — round-9 fixes must not introduce data loss
  • (cli) complete review round 5 — merge --abort after --no-commit (#4), cherry-pick/revert -n deletions (#5)
  • (cli) address review round 5 — mv hardening, rebase merge regression, +more
  • (cli) address review round 4 — mv data-loss/overlap, cherry-pick -n conflict, empty merge commit
  • (transport-connect) wire ConnectTransport into the shared retry/backoff driver (#801)

Documentation

  • resolve corpus-wide style-guide drift (#787) (#812)
  • promote SPEC-REFS to stable, document flock-over-NFS caveat, tier sigstore as roadmap (#741)
  • spec remediation WIP (index v1 drop, attest sha256 digests, ref-prefix boundary fix) (#669)
  • (core) explain why the stdio deny is per-crate, not a workspace lint (#441 follow-up) (#601)
  • release readiness — SPEC Invariants, docs/specs/ move, runbook accuracy (#548)
  • (rpc) correct stale proto comments and fastcdc fixture metadata (#391) (#433)
  • (specs,parity) SPEC-WORKTREE + worktree parity docs (#493 Phase 4) (#591)
  • (cli) unlink private items from push_branch's public rustdoc (#569)
  • (cli) reconcile help/docs/completions for the parity push; clippy clean
  • reflect --contains/--no-contains default-HEAD in all surfaces

Features

  • (cli) add log --author/--grep/--since/--until/--no-merges/--first-parent and diff -w/-b/-U<n> (#746)
  • (core,transport) centralize retry/backoff, close SSH/enc retry gap (#764)
  • (core,transport) additive Transport streaming API with PackChunk client/server streams (#760)
  • (core) pack payload compression via zstd (SPEC-PACKFILE v2) (#674)
  • (core) pair changed small blobs against their same-path prior version (#646a) (#673)
  • (core) [breaking] cut pack-shard Reed-Solomon hasher over from Sha256 to Blake3 (#671)
  • (core,cli) cross-worktree gc roots + tree-spanning gc lock (#493 Phase 3) (#590)
  • (cli,core) mkit worktree add/list/remove/prune (#493 Phase 2) (#589)
  • (core,cli) linked-worktree on-disk model + discovery (#493 Phase 1) (#592)
  • (blame) content-addressed --ignore-rev-precise fall-through (#496) (#523)
  • (transport) bound pack-chain depth via periodic re-baseline (#406) (#521)
  • (transport) skip already-applied packs on incremental fetch via local applied-pack record (#409) (#520)
  • (blame) make -M/-C and --ignore-rev merge-aware
  • (blame) merge-aware walk by default; --first-parent restores linear (#458)
  • (blame) --reverse <start>..<end> — forward (reverse) blame (#459)
  • (blame) --ignore-rev / --ignore-revs-file — skip noise commits (#457)
  • (blame) -M / -C move & copy detection
  • (blame) -w / --ignore-whitespace (ignore whitespace when matching)
  • (cli) exact rename detection in status and diff (#439)
  • (transport) atomic branch+packmap advance seam (#408, client)
  • (core) [breaking] merkelize ChunkedBlob & Tree identity (BMT roots) [stacked on #401] (#414)
  • (transport) wire delta encoding into the push/fetch path (#401)
  • (cli) stash pop/apply --index (restore staged state)
  • (rpc,repo-worker) publish mkit-rpc and mkit-repo proto modules to the BSR (#753)
  • (cli) verify commit/remix/tag signatures on clone/pull/fetch, fail closed by default (#743)
  • (attest) implement external-signer PinPrompt/PinResponse round-trip; deprecate --pin on argv (#745)
  • (transport-http) atomic advance_refs override → vcs /refs/advance (#408)
  • (transport-s3) implement S3/R2 multipart upload for packs above the single-PUT cap (#758)
  • (vcs-worker) reference mkit.transport.v1 Cloudflare Worker server (#754)
  • (transport) native ConnectRPC client for mkit+https:// (mkit.transport.v1) (#762)
  • (cli) honest transfer-progress reporting for clone/push/pull/fetch (#744)
  • (cli) unify --format=json across mutating commands (#747)
  • (cli) fetch/pull --all, clone -b/-o, config --unset and scope selectors (#740)
  • (cli) mkit serve --http — self-hosted mkit.transport.v1 Connect remote (#761)
  • (cli) mkit trust command family + verify --trusted signer binding (#739)
  • (transport) clean up applied-packs record on remote remove/rename (#545) (#597)
  • (cli) mkit self update — in-place updates verified by the release attestation (#509)
  • (blame) support -L line ranges and a [<rev>] argument (#460)
  • (cli) diff color output (--color/--no-color)
  • (cli) switch/merge-base/rev-list + high-value parity flags
  • (cli) global -C <path> and -c <key>=<val> flags (git parity)
  • (cli) git-shaped local-mutation summaries + status long format
  • (cli) git-shaped sync output (push/pull/fetch/clone/remote)
  • (cli) mv supports directory moves
  • (cli) branch --list <pattern> glob filtering
  • (cli) close git CLI/UX parity gaps + reconcile all docs
  • (transport,repo-worker,vcs-worker) add grpc.health.v1.Health check RPC (#808)
  • (mkit-transport-connect) split client timeout into per-verb-class defaults (#802)

Performance

  • (core) tune parallel_io's worker-pool cap for fsync-bound batch commits (#867)
  • (transport) split an oversized push plan across multiple packs (#835)
  • (core) stream chunked-blob ingest and checkout instead of whole-file buffering (#834)
  • (core,cli) fix quadratic staging in mkit add via path->position index (#749)
  • (core) skip BLAKE3 re-verification on display-only reads (#625) (#654)
  • (cli) load each diffstat side once — LoadedBlob replaces per-view store reads (#624) (#628)
  • (cli) render diffstat from blob metadata — stop reassembling chunked blobs (#606) (#613)
  • (blame) address review — skip blob load on fast path, borrow parent lines
  • (transport) load/persist applied-packs record once per fetch (#546) (#598)

Refactor

  • (cli,core) decompose diff.rs, worktree.rs, and store.rs hot clusters into focused modules (#633) (#656)
  • (core,cli) introduce RepoLayout path-resolution seam (#493 Phase 0) (#587)
  • (blame) dedup parent-lines load, bundle merge-pass args
  • (blame) address review — Rc-shared memo, extract walk module
  • (blame) index-backed detection, work-stack, no perf/recursion cliffs
  • (blame) sub-block move/copy detection; extract detector; typed opts
  • (blame) collapse to one checked matcher; strip vertical tab
  • (blame) extract match_lines_with_options; blame completions
  • (transport-http) extract ref endpoints/DTOs into ref_ops module (#423) (#536)

Bisect

Blame

  • --porcelain / --line-porcelain output (#524) (#541)
  • implement -C -C -C whole-history copy search (#526) (#539)
  • break -C copy ties by git line-identity (ancestor source) (#527) (#538)
  • expose inline -M<num> / -C<num> similarity thresholds (#525) (#537)

Deps

  • bump commonware-* to 2026.7.0, fix downstream breakage (#856)
  • (rust) migrate to buffa 0.8.0 (regen mkit-rpc; repo-client/worker stay 0.7) (#491)
  • (rust) bump regex (#544)
  • (rust) bump chacha20poly1305 from 0.10.1 to 0.11.0 in /rust (#483)

Proto

  • (rpc,repo-worker) extract shared RefExpectation/RefEntry to mkit/common/v1/refs.proto (#734)

Added

  • BSR-published proto modules for external integrators. The mkit-rpc schemas (common.proto/signer.proto/ssh.proto/verify.proto) and apps/repo-worker's repo.proto (RepoService) are now named modules in the repo-root buf.yaml v2 workspace, pushed to the Buf Schema Registry as buf.build/officialunofficial/mkit-rpc and buf.build/officialunofficial/mkit-repo on every tagged release (new buf-push job in crates-publish.yml, dormant until BUF_TOKEN / BUF_PUBLISH_ENABLED are provisioned — see docs/RELEASE.md). Third-party signer integrators (HSM/TPM vendors, custodial signing services) and RepoService clients can now buf generate typed bindings from a pinned tag instead of vendoring this repo — see the checked-in buf.gen.yaml reference recipes in each module's proto directory. contrib/signers/mkit-sign-se's checked-in Swift bindings are now regenerated via scripts/regen-mkit-sign-se-swift.sh (buf generate, not raw protoc) and refreshed to include the previously-missing ALGORITHM_BLS12381_THRESHOLD case.
  • mkit serve --http <addr>: self-hosted Connect remote (SPEC-TRANSPORT-CONNECT). mkit serve can now host mkit.transport.v1.TransportService over axum/HTTP instead of the SSH-frame protocol, behind the new http-transport cargo feature — an operator without SSH access or a cloud object store can run a real, testable mkit+https:// remote against a local repository (server-side half of the mkit-transport-connect crate, behind that crate's own server cargo feature; generic over any mkit_core::protocol::Transport backend; instantiated today over FileTransport). All seven wire RPCs (ListRefs/ReadRef/UpdateRef/AdvanceRefs/PackExists/ UploadPack/DownloadPack) run the underlying Transport call on a blocking task so a synchronous CAS ref write never stalls the async executor. UploadPack/DownloadPack validate the full header-then-chunks stream (offset contiguity, declared-vs-received length, BLAKE3) before ever touching storage, so a rejected upload never creates or overwrites the destination pack, and a download either completes or fails before any message is sent — never a partial stream. Fail-closed, mirroring --listen-enc: refuses to bind unless --http-token/MKIT_API_TOKEN (checked in constant time on every unary and streaming RPC) or the explicit --unsafe-allow-any-http-peer development escape is supplied; Ctrl-C/SIGTERM drain in-flight requests before exiting. The mkit.transport.v1 proto (proto/mkit/transport/v1/transport.proto) is generated via buffa/connectrpc-build, vendored under generated/ the same way mkit-repo-client/apps/repo-worker already do — see docs/specs/SPEC-TRANSPORT-CONNECT.md. A hosted reference-Worker deployment is a separate, later change (#699).
  • Honest transfer-progress reporting for clone/push/pull/fetch (#711). These commands now stream a live progress line on stderr while the network transfer runs — Writing objects: N objects, B bytes while building/uploading the outgoing pack, Unpacking objects: N objects while applying a downloaded one — using only real counts (objects actually staged/unpacked, bytes actually handed to the transport). mkit still never fabricates git's Enumerating/Counting/Compressing objects or Total N (delta D) lines, per docs/PARITY.md: mkit's transport is one-object-per-pack and computes no cross-branch delta graph. Progress shows only when stderr is a tty; the new -q/ --quiet flag on all four commands forces it off, and MKIT_PROGRESS=always/never overrides the tty auto-detection explicitly (mirrors NO_COLOR/CLICOLOR_FORCE).
  • Native ConnectRPC client for mkit+https:// (SPEC-TRANSPORT-CONNECT, #701). The mkit-transport-connect crate's mandatory baseline (always compiled, independent of its server feature above) is now a non-wasm ConnectRPC client for mkit.transport.v1.TransportService, generated from proto/mkit/transport/v1/transport.proto via the same vendored-codegen pattern mkit-repo-client/apps/repo-worker use (no protoc needed on the default build path; MKIT_REPO_CODEGEN=1 plus scripts/regen-transport-proto.sh to regenerate). mkit-cli's remote_dispatch now constructs this transport for mkit+https:// / loopback mkit+http://, replacing mkit-transport-http's bespoke JSON dialect there. TLS trust is pure-Rust (webpki-roots, no OS trust store); the synchronous Transport trait is bridged to the async generated client via a dedicated per-instance tokio runtime, mirroring mkit-transport-enc's TokioExecutor. ConnectTransport:: supports_atomic_advance() defaults to false (opt in via with_atomic_advance(true)) until a confirmed-transactional reference deployment exists. mkit-transport-http is NOT removed: its sparse-checkout/pack-shards extensions have no mkit.transport.v1 equivalent yet.
  • Transport: additive streaming pack transfer (upload_pack_streaming / download_pack_streaming). Two new opt-in trait methods move a pack as a sequence of bounded-size PackChunk { offset, data, last } segments instead of one Vec<u8>, reusing the same chunk shape mkit-rpc's ssh.proto already defines. Both have default implementations expressed in terms of the existing whole-buffer upload_pack/download_pack (buffer-then-delegate for upload, delegate-then-wrap-as-one-chunk for download), so every transport gets a working implementation with zero code and none is forced to change. mkit-transport-ssh and mkit-transport-enc override both to forward chunks straight to the PackChunk frame loop they already ran internally, so a multi-GB pack streamed from disk over SSH/enc now stays in bounded memory (roughly one chunk at a time) regardless of total pack size, instead of requiring the whole pack materialized up front. mkit-transport-http keeps the default buffer-then-delegate behavior for now — real HTTP pack streaming arrives via the mkit.transport.v1 Connect service's client-/server-streaming UploadPack/DownloadPack RPCs (SPEC-TRANSPORT-CONNECT §6, pending #698/#701) — but its per-retry full-body clone in upload_pack is fixed separately: the request body is now Bytes (refcounted) instead of Vec<u8>, so a retried upload shares the same buffer across every attempt instead of copying it again per retry (#702).
  • log --author/--grep/--since/--until/--no-merges/--first-parent and diff -w/-b/-U<n> (#712). mkit log filters commits by a substring match on the author identity (--author) or commit message (--grep), by a --since/--until timestamp bound (accepting @<unix-seconds>, now/today/yesterday, <N> <unit> ago, or YYYY-MM-DD[ HH:MM:SS]), hides merge commits from the output (--no-merges), or walks only first parents so a merged side branch never enters the walk at all (--first-parent, stronger than --no-merges). All filters apply before -n's limit. mkit diff gains -w/--ignore-all-space and -b/--ignore-space-change (whitespace-insensitive line comparison; -w wins if both are given) and -U<n>/--unified=<n> (context-line count, default 3). Both --author/--grep are plain substring matches rather than regexes (mkit identities are opaque, not free-text names) and --since/ --until use a small explicit date grammar rather than git's approxidate — documented divergences, not gaps.
  • Windows release target (x86_64-pc-windows-msvc) and PowerShell installer. release.yml's build matrix now ships a fifth leg — windows-latest, .zip archive instead of .tar.gz, mkit.exe — built with the backend-windows-credential keystore feature (already the default on Windows via mkit-cli's [target.'cfg(windows)'.dependencies] stanza), matching parity with the already-tested windows-credential leg of rust.yml's manual-only keystore-backends matrix. The Windows archive goes through the same cosign keyless signing and mkit-native DSSE release attestation as every other target. New install.ps1 at the repo root is the native-Windows counterpart to install.sh (same trust model: cosign-required by default, downgrade guard, atomic install) — served at https://mkit.sh/install.ps1 alongside the existing install.sh. New contrib/scoop/mkit.json manifest template (mirrors contrib/homebrew/mkit.rb) un-defers the Scoop-manifest checklist item in docs/RELEASE.md. install.sh itself still targets Darwin/Linux only — it now points MINGW/MSYS/Cygwin users at install.ps1 instead of failing with an unsupported-OS error. Known gap: mkit self update does not yet support the Windows install (it hardcodes .tar.gz/tar+gzip extraction and resolves its state dir via $HOME, which is commonly unset on native Windows) — Windows users should reinstall via install.ps1 or a future Scoop bucket instead of mkit self update for now (#714, part of #676).
  • SLSA build provenance for release archives. The release job in .github/workflows/release.yml now generates a standard SLSA build provenance attestation (actions/attest-build-provenance, GitHub/ Sigstore-native) over every dist/*.tar.gz and dist/*.zip archive (including the Windows leg), staged alongside the existing cosign signatures and the mkit-native DSSE attestation as mkit-X.Y.Z.provenance.jsonl. This is additive — it does not replace cosign or the mkit-native attestation — but gives downstream tooling (gh attestation verify, slsa-verifier) a recognized, off-the-shelf provenance format to check against instead of parsing mkit's bespoke DSSE predicate. See docs/RELEASE.md ("Verify the SLSA build provenance attestation").
  • Packfile v2: per-entry zstd compression (SPEC-PACKFILE §3.3, §3.4). PackWriter/PackReader transparently compress/decompress pack entries — two new entry types, 0x03 zstd-raw and 0x04 zstd-delta, each carrying its own independent zstd frame (no shared dictionary, no whole-pack stream, so existing framing/caps/trailer semantics are unchanged and decompression memory is bounded to one entry at a time). No call-site changes needed: push_raw/push_delta compress a candidate payload when it is at least 64 bytes and compresses strictly smaller on the wire (mirroring the existing delta-preference gate's posture), using zstd level 3 (library default). The writer emits version = 1 when a pack has no compressed entries and version = 2 the moment it has at least one; 0x03/0x04 are illegal inside a version = 1 pack and rejected as InvalidEntryType if seen there. Old (pre-v2) readers hitting a v2 pack fail closed with UnsupportedVersion — the intended behavior, not a bug. Decode is bomb-guarded: the claimed decompressed length is checked against the 1 GiB object cap before any decompression allocation, decompression is capacity-bounded to that claim, and the actual decompressed length is re-checked against the claim afterward (new PackError::DecompressedSizeOverCap / DecompressedSizeMismatch / ZstdEntryTruncated / ZstdDecompress variants). On a synthetic 6-commit / 4-file-per-commit text corpus (files 8–48 KiB, <100 KB cap), pack bytes went from 655,764 (v1 uncompressed) to 184,881 (v2 compressed) — a 3.55x reduction (71.8% saved); see rust/benches/benches/pack_compression.rs. New direct zstd dependency on mkit-core (behind a pack-zstd feature, on by default) — already present transitively via the commonware-storage dependency stack at the same resolved version (0.13.3), so this adds no new supply-chain root. mkit-wasm and apps/repo-worker (both compile to wasm32-unknown-unknown) opt out (default-features = false) since zstd-sys cannot target that platform.
  • mkit-core: CAS-guarded ref delete. New public refs primitives delete_ref_if_matches and (on --features history-mmr) delete_ref_with_history_if_matches — a delete_ref/ delete_ref_with_history that only removes the ref (and, on history-mmr builds, its journal) when its current on-disk value is exactly the caller-supplied expected hash, using the same per-ref cas_lock_name lock update_ref's Match arm already takes. mkit branch -m now routes its source-branch drop (and, on a lost race, its destination rollback) through these instead of an unconditional delete — see the Fixed entry below (#658).
  • mkit blame --porcelain / --line-porcelain. git's grouped machine-readable blame: a per-line header (<id> <orig> <final> [<group-len>]) plus a metadata block (author/committer, author-time/ -tz, summary, boundary on a file-history root, and filename) — once per commit for --porcelain, for every line under --line-porcelain — with each content line tab-prefixed. Pinned against git 2.50.1 for the in-scope fields. Documented divergences, consistent with --format=json and the log precedent: 64-hex ids; author/committer carry mkit's Identity (empty *-mail, +0000 tz, single UTC author = committer); filename is the -C copy source on a cross-file copy; git's previous line is out of scope and not emitted.
  • mkit bisect run <cmd> [args…]. Drives the bisection loop automatically: it checks out each candidate, runs the command, and classifies from the exit status using git's contract (0=good, 125=skip, 1–127 else=bad, ≥128 or signal=abort), converging on the first bad commit. The candidate is also exported as MKIT_BISECT_COMMIT. mkit's bisect stays print-candidate by design, so run checks out each candidate transiently for the test, then restores the original HEAD and prints the first bad commit rather than parking there. Each candidate is checked out with --force, so a test command that dirties a tracked file doesn't block the next iteration; and when only skipped candidates remain, run reports the result as ambiguous (like git) and exits non-zero instead of guessing.
  • mkit checkout --force / -f. Discard local changes that would block the switch (git checkout -f): skip the dirty-tracked/staged safety gate and overwrite locally-modified tracked paths with the target's version; untracked files are still preserved. Used by bisect run.
  • mkit self update. The binary can now update itself in place from a signed GitHub Release — but only when installer-managed (the .mkit-installed-tag receipt written by install.sh sits next to the executable); Homebrew/cargo installs are refused with channel-specific guidance. The downloaded archive is verified against the mkit-native release attestation (below) using release-attestation public keys embedded in the binary at build time — no cosign, no GitHub attestation API — plus the sha256 sidecar as defense-in-depth, and the staged binary must pass a version self-check before an atomic same-directory swap. Downgrade policy mirrors the installer (latest never downgrades; explicit --version pins need --allow-downgrade), receipts are rewritten in the installer's exact format, and --check/--format json report without changing anything. There is no background update check, ever. Not yet supported on Windows.
  • mkit-native release attestation. Every release now ships mkit-<ver>.release.dsse: a DSSE/in-toto v1 envelope over the BLAKE3 digests of all release tarballs, predicate .../spec/predicate/release/v1 {"tag": "vX.Y.Z"}, signed with a dedicated Ed25519 release key (public rotation set checked in at docs/keys/release-attest.pub; custody plus rotation runbook in docs/RELEASE.md). release.yml self-verifies the envelope against the checked-in public key before publishing, and the envelope is covered by the cosign-signed SHA256SUMS. Produced by the new internal mkit-release-attest tool crate (publish = false).
  • mkit blame line ranges and revision argument. blame now accepts -L/--lines to restrict output to a line range — <start>,<end>, <start>,+<n> (n lines forward), <start>,-<n> (n lines back, ending at start), <start>,, ,<end>, and a bare <start> — and an optional [<rev>] argument (mkit blame <rev> <file>) to blame the file as of any revision instead of only HEAD. Range semantics and diagnostics match git blame -L: inclusive bounds, inverted ranges swap, over-long ends clamp to EOF, the low bound is validated against EOF, and bad input reproduces git's messages (-L invalid line number: <n>, -L invalid empty range, file <f> has only N lines).
  • mkit blame -w / --ignore-whitespace. Ignores whitespace when matching lines across revisions (like git blame -w, ignoring all whitespace), so a whitespace-only edit — reindent, tab↔space, spacing tweak — no longer steals attribution; output still shows the file's current bytes.
  • mkit blame -M / -C move and copy detection. -M (--find-moves) credits a block moved within the file to its origin commit; -C (--find-copies, repeatable, implies -M) credits a block copied from another file, resolving the true origin by blaming the source file. Repeating -C widens the search from files changed in the commit to every file in the parent commit. Detection is block-based over normalized keys: the longest contiguous block above git's default thresholds (20 for -M, 40 for -C) is credited, so a moved block beside genuinely-new lines is split out and — combined with -w — a block copied with a whitespace change is still detected. Configured through a typed MoveDetection/CopyDetection API that can't express an invalid "enabled but zero-threshold" state. Detection is merge-aware (#499): at a merge both -M moves and -C -C copies are traced against every relevant parent's tree, so a block moved or copied in from a non-first-parent side is credited to that side's origin — matching git blame -M/-C, which credits the merge parent whose tree holds the source (a block whose source is only in the merge's own tree stays on the merge, as in git). The -C-at-a-merge gaps from the initial #499 landing are now closed by implementing git's actual per-parent candidate mechanism (from git 2.50.1's blame.c, each shape pinned by a test with its git recipe): the -M/-C pass runs against every real parent in commit order, first-found-wins. A parent that contains the blamed file — and whose copy of it is not byte-identical to an earlier parent's (git dedups those) — keeps its porigin: it supplies the within-file -M source, and its -C candidates are the files modified between that parent and the merge. A porigin-less parent (deleted the file, holds a duplicate blob, or the file is newly added by the merge) has no -M source and, with -C -C, gets its entire tree searched. The rules previously described as "interior vs boundary tie-breaks" fall out of this mechanism, plus the shapes they missed: an unchanged source on the first parent is invisible (block stays on the merge) even when another parent deleted the file; the same source modified at the merge credits the first parent — at plain -C level 1 too; a parent that deleted the blamed file can still supply the -C -C copy source from its tree; and a file newly added by the merge searches every real parent, first included (under --first-parent, only the first). Documented divergences from git remain: inline -M<num>/-C<num> threshold forms aren't exposed on the CLI (the core API takes a custom threshold); when one source holds the block at several offsets the earliest offset wins (git scores candidates and tracks line identity through its diff); and within a single unmatched run longer than 10,000 lines only the whole run is matched, not sub-blocks (a cost bound; the matcher already caps inputs).
  • mkit blame -C -C -C whole-history copy search. git's third -C ("copies from other files in any commit") now whole-tree-searches the parent at every walk step, not only at the commit that creates the blamed file. So a block copied into a persisting file from a source that was unmodified in the introducing commit is credited to that source's origin — previously -C -C -C was approximated as -C -C and missed it. Pinned against git 2.50.1.
  • mkit blame -C copy tie-break now matches git. When two equally-similar copy sources exist, blame credits the source that traces to the older (ancestor) commit — git's push-blame-furthest-back bias — instead of the first candidate in path order. The ordering is topological (mkit commits can share a whole-second timestamp, so ancestry, not time, is authoritative), pinned against git 2.50.1.
  • mkit blame --ignore-rev / --ignore-revs-file. Skip "noise" commits — mass reformats, license-header sweeps, renames — during attribution, like git blame --ignore-rev. A line that would be credited to an ignored commit falls through to the commit that previously changed it; a line the ignored commit genuinely inserted stays put (git's default, no marker). The fall-through is merge-aware (#499): at an ignored merge a line the first parent can't pair (it dropped that line) falls through across to the next parent that does — first-parent-wins, matching git blame --ignore-rev at a merge. --ignore-rev is repeatable and accepts any revision (short hash, ref, HEAD~2); --ignore-revs-file reads full hex object names one per line, skipping blank lines and # comments (including inline) — both verified against real git. Unknown or malformed inputs reproduce git's messages (cannot find revision <rev> to ignore, invalid object name: <token>, could not open object name list: <path>), though mkit returns its sysexits-style exit codes rather than git's blanket 128. mkit does not auto-read .git-blame-ignore-revs or a blame.ignoreRevsFile config key — pass the file explicitly.
  • mkit blame --reverse <start>..<end>. Walks history forward instead of backward, like git blame --reverse: blames the <start> version of the file and attributes each line to the last commit in the range in which it still existed (answering "which commit removed or last touched this line"). <start>.. defaults <end> to HEAD; an explicit <start> is required, and -w/-L compose with it. Verified field-by-field against real git blame --reverse (survivors → end, removed/modified lines freeze at their last commit, unchanged commits advance attribution, file-absence kills a line). Deliberate divergences: mkit reports a clear error for a missing / malformed / empty range or open <start> where git prints a cryptic "dig up from" message; a line that never survives a step is shown without git's leading ^ boundary marker (mkit's tab format has no ^); and the range is followed along <end>'s first-parent chain only (mkit blame is first-parent only — a <start> reached solely through a merge's second parent errors rather than resolving; the full-history walk in #458 would lift this).

Removed

  • Docs/dev-scratchpad cleanup for a production-mature public footprint. Removed docs/adr/ (4 ADRs, superseded by docs/specs/SPEC-MERKLE-OBJECTS.md and the code they annotate), docs/research/ (2 exploratory design notes), docs/keystore.md (a redundant stub — see docs/specs/SPEC-KEYSTORE.md), and docs/INVARIANTS.md (unreferenced anywhere in the repo, scoped to the now-closed #634 perf epic).
  • Removed the mkit-native release-attestation feature. mkit self update no longer requires, or is able to verify, the DSSE/in-toto envelope previously signed over release tarballs by the release-attest crate; verification is now sha256-sidecar-only, and a release that omits the sidecar installs with no cryptographic check at all beyond TLS. cosign signatures and the GitHub-native SLSA build-provenance attestation on every release are unaffected and remain the recommended verification path for install.sh. Breaking (SemVer, pre-1.0 minor bump): removes the public mkit_cli::commands::self_update::verify_release_attestation function and the UpdateEnv::trust_keys field. Also removes rust/crates/release-attest, docs/keys/, and the corresponding release.yml sign/verify steps; the MKIT_RELEASE_ATTEST_KEY GitHub secret is now unused and slated for decommission.
  • Operational slim-down. Removed governance/process ceremony whose maintenance overhead was disproportionate to the project's size: GOVERNANCE.md, MAINTAINERS.md, TRADEMARKS.md, SUPPORT.md, the GitHub issue/PR templates, and eight non-gating or now-obsolete CI workflows (rename-gate, crates-owners, reproducible-build, mutation-score, state-machine, supply-chain, typos, pr-title) plus their configs (typos.toml, scripts/verify-rename.sh). Also removed the completed docs/MERKELIZATION-PLAN.md (the work shipped — see docs/specs/SPEC-MERKLE-OBJECTS.md and ADR 0001). The license grant (LICENSE-MIT, LICENSE-APACHE, NOTICE), SECURITY.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, and the geiger unsafe-code ceiling check are retained.

Changed

  • commonware 2026.7.0 dependency train across the workspace (up from 2026.5.0), plus apps/repo-worker and the separate contrib/signers workspace, both of which path-depend into rust/crates/* and needed their own Cargo.lock regenerated too. No wire-format or object change — investigated dropping mkit-core/src/merkle.rs's vendored BMT construction for a direct commonware_storage::bmt dependency now that bmt is confirmed no_std (commonwarexyz/monorepo#4090), but found it's not viable: bmt::Builder<H: Hasher> requires commonware_cryptography::Hasher, and commonware-cryptography's blst (BLS12-381) dependency is unconditional, not feature-gated, and fails to build for wasm32-unknown-unknown on toolchains without a WASM-capable C compiler. The vendored construction stays (its cross-check test against upstream still passes byte-for-byte). commonware-parallel's Strategy trait gained new required methods (manual, spawn, run, try_run, try_fold, sort_by); mkit's internal CountingStrategy test helper was updated to match. commonware_utils::test_rng_seeded(seed) was renamed to TestRng::new(seed) and commonware_storage::freezer::Freezer::init gained a third Option<Checkpoint> parameter; both updated at their mkit call sites.
  • BREAKING (mkit-attest, bls-threshold feature): trusted_dealer/bls_threshold_trusted_dealer's generic RNG bound moved from rand_core 0.6's CryptoRngCore to rand_core 0.10's CryptoRng — a from-scratch, source-incompatible trait (TryRng/ TryCryptoRng), not just a version bump. commonware-cryptography 2026.7.0 made the same move internally, so this crate's bound had to follow. Callers supplying an RNG type must now implement rand_core 0.10's CryptoRng (for example commonware_utils::TestRng, or rand_core::UnwrapErr(getrandom::SysRng) in place of the old rand_core::OsRng); an RNG that only implements the 0.6-era CryptoRngCore no longer satisfies the bound.
  • BREAKING (mkit-core, pack-shards feature): pack-shard Reed-Solomon hasher cut over from Sha256 to Blake3. pack_shard::RsScheme now wraps commonware_coding::ReedSolomon<Blake3> instead of Sha256, matching the hasher mkit uses everywhere else and dropping a redundant per-shard SHA-256 pass inside commonware's own Merkle-tree build (it was already fully separate from this module's BLAKE3 shard_hashes envelope check, which is unchanged). MANIFEST_VERSION is bumped 0x01 → 0x02 since the wire-visible ShardSet::commitment value changes; this is a hard cutover, not dual-hasher support — a 0x01 manifest is rejected with a version-specific error ("manifest version 0x01 (Sha256-era) — re-shard with a current mkit") instead of the generic unsupported-version message, and 0x01 is retired permanently. Producers and consumers on different pack-shards-era mkit versions cannot interoperate; re-shard with a current build. docs/specs/SPEC-PACK-SHARDS.md §2.1/§4 updated to match, and §4's stale "Sequential parallel strategy" phrasing (left over from #653, which made the strategy a caller-selectable parameter) is corrected alongside it (#661).
  • SPEC-WORKTREE.md plus worktree parity docs (#493 Phase 4). New normative spec covering the common-dir/per-worktree state split, the mkitdir: pointer-file format, the worktrees/ registry, discovery (and its fail-closed matrix), the lock model, and cross-tree gc semantics. git worktree leaves PARITY.md's v1 non-goals list with a scope amendment recording the deliberate divergences (per-tree stash; move/lock/repair remain follow-ups); SKILL.md, the man page, and the bash/zsh/fish completions gain the worktree command.
  • Cross-worktree gc plus the lock split (#493 Phase 3). mkit gc is worktree-aware: root collection unions HEAD, staging index, ORIG_HEAD, in-progress merge/cherry-pick/revert/rebase state, conflict sidecars, and the tree-local stash across the main tree AND every registered linked tree (including prunable-but-unpruned entries — until worktree prune reaps a state dir, whatever it pins stays pinned), and fails closed if the registry or any sibling's state cannot be read. gc's "shared lock spanning trees" is the union of all per-tree worktree locks, acquired in deterministic order (main first, then registry ids ascending), so gc serializes against worktree/index mutations in every tree — a gc run blocks (then TEMPFAILs) while any sibling is mid-mutation. The Phase 2 interim refusal of gc-with-linked-worktrees is lifted. Tree-locality of status/reset --hard/clean/rm/stash across trees is pinned by tests.
  • mkit worktree add/list/remove/prune (#493 Phase 2). Linked working trees with git's semantics: add <path> [<commit-ish>] creates a tree sharing the one object store and the shared refs (default: a new branch named after the path's basename; a branch argument checks it out; any other revision detaches), list (--porcelain for scripts) shows every tree with its HEAD, remove deletes a tree but refuses to destroy local changes, untracked files, or an in-progress operation without --force, and prune reaps registry entries whose tree vanished (--dry-run previews). A branch may be checked out in at most one tree: worktree add, checkout/switch, branch -d, and branch -m all refuse with already checked out at '<path>' (branch moves are single-writer through the history-MMR ref path). Registry mutations serialize on a new common-dir worktrees.lock; add orders its writes so a crash leaves at worst a prunable orphan, never a live tree pointing at half-built state.
  • Linked-worktree on-disk model plus discovery (#493 Phase 1). mkit_core::layout gains the linked-worktree groundwork: a linked tree's .mkit is a pointer FILE (mkitdir: <path>, the analog of git's gitdir: file) naming its per-tree state dir under the main repository's .mkit/worktrees/<id>/ (with commondir and a mkitdir back-pointer inside, git-style); layout::discover resolves it — a .mkit directory or absent .mkit still yields the classic single-worktree layout byte-identically, while a malformed, oversized, or dangling pointer fails closed with a typed DiscoverError. The CLI's commands::resolve_layout now performs this discovery, so every command already works from inside a linked tree: commits write into the one shared object store and move shared refs, while HEAD/index/op-state/stash stay in the invoking tree's state dir. Repo-relative signing-key paths (.mkit/keys/…) resolve against the shared common dir so linked trees sign with the same repo keys. No user-facing worktree command yet (that lands with mkit worktree add/list/remove/prune); linked trees can only be assembled by hand at this stage.
  • RepoLayout path-resolution seam (#493 Phase 0). mkit-core gains layout::RepoLayout, the single authority for resolving repository state under .mkit/, classifying every path as common-dir state (objects, format marker, refs, shallow, config, keys, history MMR, recovery log, attestations, applied-packs, git-bridge state, sparse bitmap cache, pack-shard output) or per-worktree state (HEAD, index, ORIG_HEAD, merge/cherry-pick/revert files, conflict sidecar, rebase-apply/, bisect, stash, sparse-checkout filter, worktree lock) in preparation for linked working trees (mkit worktree, later phases). Repo-state APIs now take &RepoLayout instead of a bare &Path root: ObjectStore::{open,init}, all of refs, index::{read_index,write_index,index_path}, ops::{conflict_state, rebase, bisect, stash, gc, recovery}, ops::restore::{load,write}_ sparse_checkout, CommitHistory::open_at (and CommitHistory::common_dir() replaces CommitHistory::mkit_dir()), mkit_attest::store, and mkit_git_bridge::map::state_dir. In the classic single-worktree layout every resolved path is byte-identical to before — zero behavior or on-disk change; goldens unchanged. The CLI resolves its layout once per command through commands::resolve_layout, the future discovery seam.
  • buffa 0.8.1 plus connectrpc 0.8.0 everywhere. The main workspace and contrib/signers move buffa 0.8.0 → 0.8.1 (runtime-only patch; vendored codegen verified byte-identical). The ConnectRPC pair (mkit-repo-client plus apps/repo-worker) finally leaves its 0.7 pin: connectrpc 0.8 ships on buffa ^0.8.1, so both crates now match the rest of the repo on a single buffa version (the workspace lockfile drops the entire duplicate 0.7 dependency tree). Vendored ConnectRPC codegen was regenerated with the 0.8 toolchain — ~4,300 lines smaller thanks to buffa 0.8's impl_default_instance! runtime macros, fused put_*_field writers, and shared check_wire_type/map_codec helpers (wire output is byte-identical). Handler traits now return impl Encodable<Resp>, letting future handlers return zero-copy views. connectrpc's now-optional json feature is enabled explicitly so generated serde derives and JSON error/end-of-stream frames keep working on wasm.
  • mkit blame is now merge-aware by default (git parity). Blame walks the file's whole ancestor subgraph instead of only the first-parent chain: at a merge, each line is credited to the first parent that still contains it, so a line merged in from a side branch is attributed to the commit that actually wrote it rather than to the merge commit — matching git blame's default. This changes output for histories with merges; the new --first-parent flag (like git blame --first-parent) restores the previous first-parent-only attribution and composes with -w/-M/-C/--ignore-rev. Verified field-by-field against real git (distinct side-branch lines, first-parent-wins on a line added identically on both sides, evil-merge lines credited to the merge, octopus merges). This is also the prerequisite for provable blame (#495), which needs correct merge-aware attribution before it can be made verifiable. (mkit still omits git's ^ boundary marker and uses sysexits exit codes, not 128.)
  • BREAKING (mkit-core): Tree and ChunkedBlob are now content-addressed by a domain-bound Binary Merkle Tree (BMT) root (id = domain_digest(TYPE_DOMAIN, bmt_root)) instead of BLAKE3 of their serialized bytes; every other object type keeps the flat scheme. Because a Tree id feeds its Commit id which feeds every ref, this re-addresses all history. Content addressing is preserved (the id is still a deterministic function of canonical content) and stays tamper-evident (read recomputes the root), and the serialized wire format and schema_version = 0x01 are unchanged — the break is in object identity only. Cross-format safety is a mandatory .mkit/format repo marker (bmt-v1): a pre-merkle repository is rejected at open (IncompatibleRepoFormat) instead of silently mis-reading every Tree/ChunkedBlob. Pre-1.0 API/format break, no migration. New normative spec docs/specs/SPEC-MERKLE-OBJECTS.md pins the construction (#414).
  • BREAKING (mkit-core): in the blame module, the public type alias BlameResult2<T> was renamed to BlameOutcome<T>, and the unbounded match_lines function is now private — line matching is an internal, size-checked detail of blame_file_with. Both are pre-1.0 API breaks; no in-workspace consumers were affected. (release-plz's semver_check enforces the matching version bump at release time.)
  • BREAKING (mkit-core): dropped .mkit/index v1 read compatibility. deserialize now accepts only the current stat-cached format (FORMAT_VERSION = 0x02) and rejects every other version byte, including the legacy v1 (0x01, 35-byte entries with no stat cache). The index is repo-local, advisory, and never exchanged between peers (SPEC-INDEX §1), so this carries no cross-peer compatibility obligation; there is no migration path for a stray v1 file, since mkit never shipped a release that wrote one. Pre-1.0 API/format break.
  • BREAKING (mkit-attest): Subject gained a new required field, digest_sha256_hex, and sha256_hex() was added to compute it. Every subject now carries a sha256 digest alongside blake3 (SPEC-ATTESTATIONS §4.2) — both digests of the identical underlying bytes — so cosign, gh attestation verify, and the SLSA verifier (all of which only read the in-toto/SLSA DigestSet sha256 key) can read mkit attestations. sha2 moves from optional (gated behind algo-secp256k1/algo-p256) to an unconditional mkit-attest dependency. Pre-1.0 API break; every caller constructing a Subject (git.rs, git_import.rs, self_update.rs, release-attest) was updated.

Performance

  • WriteBatch::commit's barrier-issuing thread pool is now sized for I/O concurrency, not CPU cores (#864). The pool that issues one fsync-class barrier per staged object was capped at available_parallelism().min(16) — CPU core count, the wrong resource for a workload that spends nearly all its time blocked on the storage device rather than computing (mirroring why tokio::task::spawn_blocking defaults to 512 threads for the same class of work). Replaced with a benchmarked MAX_SYNC_WORKERS = 64 constant. Real effect on a 1000-file add/commit: mkit goes from ~1.6x slower than git2 (libgit2) to roughly matching it, while its lead over the actual git CLI widens to ~21.5x. No change to WriteBatch's durability contract or public API.
  • Multi-pack push splitting (#831): a push whose plan exceeds a single pack's payload cap (pack::MAX_TOTAL_PAYLOAD, 4 GiB in production) now splits across multiple packs instead of failing with PackfileTooLarge. remote_dispatch::build_and_upload_packs seals and uploads each pack as soon as it would exceed the cap (peak memory: one pack buffer, not the whole plan), and advance_packmap now chains an ordered set of pack keys onto a single packmap node instead of exactly one — the fetch side needed no changes, since resolve_pack_chain already flat_maps every node's packs. A push whose plan fits in one pack (the overwhelming common case) is unaffected. Also fixes a latent progress-reporting bug: PackUploaded byte counts now accumulate across packs instead of being overwritten by the last one. This closes the remaining format/planning gap left by #704 (S3/R2 multipart) and #702/#647 (streaming pack transfer), which had already removed the transport-side reasons for the 4 GiB cap without giving push planning a way to exceed it.
  • Streaming chunked-blob ingest and checkout (#828): add/build_tree no longer read a large file's entire contents into memory before FastCdc-chunking it — a new chunker::ChunkReader streams the content-defined cut algorithm off a Read source, keeping at most one chunker window (256 KiB) resident regardless of file size (byte-identical boundaries to the in-memory ChunkIterator, pinned by proptest). worktree::hash_file_with_metadata is now the single streaming-capable entry point shared by mkit add and build_tree. Checkout got the matching fix: restore_blob streams each ChunkedBlob chunk straight to the destination file via a new create_tmp_for_write/finish_atomic_write pair instead of concatenating the whole reassembled file into one buffer first. Object hashes, chunk boundaries, and file contents are unchanged — this only bounds ingest/checkout memory, laying the groundwork for supporting files larger than today's 1 GiB cap.

Fixed

  • branch -m racing commit on the same branch could silently lose the commit. #637 serialized Match-conditioned ref writes under a shared per-ref lock, but commit's ref advance still used RefWriteCondition::Any (an unconditional clobber) and branch -m's delete of the renamed-away source ref was unconditional too — neither side went through Match, so the shared lock never engaged between them. A rename could read a branch's tip, let a concurrent commit land on top of it via its own CAS, and then delete the ref anyway, destroying the just-landed commit with both commands reporting success. commit's advance_head now writes Match(expected_tip) / Missing instead of Any (aborting with a clear, GC-recoverable TEMPFAIL if the branch moved underneath it since composing the message), and branch -m now deletes the source ref via the new CAS-guarded delete_ref_if_matches, rolling back the just-created destination and erroring instead of destroying a concurrent commit (#658).
  • listRefs(prefix) no longer matches across a ref-name component boundary in mkit-transport-memory and mkit-transport-s3. A request for prefix refs/heads/feat incorrectly matched refs/heads/featx (a bare string-prefix check with no boundary enforcement), returning the malformed suffix "x". Both transports now require the match be followed by / or end-of-string, per SPEC-REFS's normative listRefs algorithm; a ref that merely shares a string prefix with the query, without extending it at a / boundary, is excluded entirely rather than truncated into a bogus name.
  • CAS conflicts over mkit serve now surface as RefConflict. Per SPEC-TRANSPORT §4.2.1 the server answers a compare-and-swap mismatch on updateRef with Error{INVALID_REQUEST} carrying the current ref value in Error.details; previously mkit serve collapsed every update_ref failure into a generic invalid-request with empty details, so a genuine conflict degraded to RemoteError on the SSH client (whose classifier requires the current-id payload). Both the stdin/stdout SSH server and the encrypted listener now emit the spec shape via the shared verb handler, and the enc client was tightened from a looser presence-based rule to the same strict details-based classification the SSH client uses (shared mkit_rpc::map_update_ref_error), so genuine invalid requests no longer misclassify as conflicts (#551).
  • External-signer PinPrompt/PinResponse round trip is now implemented; mkit-sign-ctap --pin on argv is deprecated. SPEC-EXTERNAL-SIGNER §4 specifies an in-band PIN round trip so a hardware signer can request a PIN mid-sign without it ever touching argv, but ExternalSigner only wrote Hello/SignRequest and rejected any PinPrompt frame with ExternalSignerBadResponse — the only way to supply a PIN was the reference CTAP signer's plain --pin flag, readable by any other local user via ps / /proc/<pid>/cmdline (the same exposure class docs/THREAT-MODEL.md §3.2 defends key-file confidentiality against). ExternalSigner now keeps the child's stdin open for the whole sign conversation and answers a PinPrompt via a new PinProvider trait (default TtyPinProvider: an interactive terminal prompt, best-effort no-echo via stty on Unix — never argv or an environment variable), bounded to 8 round trips per conversation. mkit-sign-ctap now requests a PIN in-band when the authenticator needs one and prints a deprecation warning to stderr when --pin is passed (#694).

Internal

  • Open-source / publish readiness sweep: scrubbed internal identifiers, fixed stale manifest/rustdoc claims, deduplicated copy-pasted helpers (CLI emit_err, transport pubkey decoders, replay/rollback plumbing), decomposed oversized modules (keystore::software, CLI serve), and fixed several latent bugs (S3 ref pagination, atomic bisect-state write, BLS external-signer fail-closed) with regression tests.

Security

  • RUSTSEC-2025-0055 ignore removed from deny.toml. The advisory (tracing-subscriber 0.2 ANSI-escape logging, flagged for re-justification by 2026-08-21 per the [0.2.0] entry below) reached the tree only transitively via commonware-runtime 2026.5.x → arkworks (ark-ed-on-bls12-381-bandersnatch → ark-r1cs-std) → tracing-subscriber 0.2. The commonware 2026.7.0 bump drops that entire arkworks/ bandersnatch chain from the dependency tree; the only tracing-subscriber left is v0.3.23, the fixed line. RUSTSEC-2024-0436 (paste, unmaintained) is still transitively present via commonware's feature-gated deps and remains ignored.
  • clone/pull/fetch now verify commit/remix/tag signatures and fail closed by default. Previously the only signature check was the manual, single-revision mkit verify <rev> — a hostile remote (THREAT-MODEL §3.1) could push an entirely unsigned or forged history and every clone/pull/fetch would accept it with zero indication anything was wrong. Every commit/remix/tag a fetch newly introduces is now run through mkit_core::sign::{verify_commit,verify_remix,verify_tag} — the exact check mkit verify runs manually — before the remote-tracking ref is published; a structurally invalid or missing signature aborts with exit 65 and leaves local refs/working tree untouched. Only the newly-fetched delta is checked (bounded per-fetch cost), and the new DispatchError::UnsignedOrInvalidObject is deliberately excluded from the applied-pack self-heal retry. Opt out per invocation with --no-verify-signatures, or persistently via the user-scoped pull.require_signed = false config key (added to REPO_FORBIDDEN_KEYS — a cloned repo's own config cannot disable the check that protects the clone against exactly that repo). A new message-only mkit.rpc.v1.verify proto (verify.proto, co-located with signer.proto) documents the verification contract so a future ConnectRPC transport (for example, apps/repo-worker) can bind the identical check instead of reimplementing it (#692).
  • SSH trust-pinning is now actually enforced. The per-repo ssh.strict_host_key_checking, ssh.user_known_hosts_file, and ssh.identity_file keys were parsed into Config but never threaded into the spawned ssh(1) process, so the documented control was inert (a documented-but-absent security control). They are now mapped into SshOptions and passed to the child as -o StrictHostKeyChecking=… -o UserKnownHostsFile=… -i …, matching docs/SSH-SECURITY.md §3. Pure producer-side wiring — the transport already consumed SshOptions (#389).

0.3.0 - 2026-06-15

Performance

  • Hardened after multi-agent review (16 findings, all resolved): worktree snapshots for status/diff/safety checks moved to an in-memory EphemeralSink (no flush cost, no garbage objects, and no visible-but-unflushed object can poison content-addressed dedup — SyncPolicy::None removed); per-file write barriers are real on every platform (fdatasync/FlushFileBuffers, not just Apple's F_BARRIERFSYNC) so batch durability no longer assumes ext4 ordered-data journaling; durability.objects = per-object config key exposes the strict schedule; the index stat cache gained ino+ctime fields (catches replace-by-rename and touch -r), a per-entry racy window for coarse-timestamp files, and status heals the cache from hash-time observations (never stat-after-verify) and never auto-upgrades a v1 index; stash pop records the popped commit in the recovery log before dropping its manifest entry; status/diff snapshots type-validate staged blobs from the 6-byte prologue instead of full read+re-hash, while commit and the other tree-publishing paths hash-verify each staged object before a tree references it (a corrupt staged object can never be published); pack unpack no longer double-hashes every object.
  • Batched durability (WriteBatch): object writes from one command (add, commit, pack unpack, stash) are staged invisibly and made durable together at a single commit point — exactly 2 full flushes per command (one over staged data, one terminal device flush) instead of 2 per object, with per-file/per-dir writeback barriers issued from a scoped-thread pool. Same crash invariant as before, stated in SPEC-OBJECTS §10.1: an object is never visible before its bytes are durable, and refs/index are only written after their referents are durable. SyncPolicy::{PerObject,Batch} selects the schedule (query snapshots use an in-memory EphemeralSink, not a sync policy); flush counts and ordering are pinned by unit tests. Measured on an M4 Max (APFS): add+commit of a 100 MiB file 13.5s → 0.75s; 100 × 10 KiB files 1.1s → 0.18s.
  • Index v2 stat cache: .mkit/index entries now carry mtime_ns+size (SPEC-INDEX v2; v1 indexes read fine and upgrade on first write). add/status/commit -a prove unchanged files by stat instead of re-reading and re-hashing content — O(stat), with git's racy-clean rule applied at read time. status with an unchanged 100 MiB file: 113ms → 13ms.
  • Zero-copy ingest: chunk and small-blob writes stream prologue ‖ payload straight from the source buffer (serialize::blob_prologue), eliminating two memcpys per chunk; status/diff snapshots use an in-memory EphemeralSink and no longer pay any durability cost; worktree::hash_file_object content-addresses without writing, so change detection no longer mutates the store.
  • Checkout: restored worktree files keep tmp+rename atomicity but are no longer flushed per file (the store is the source of truth and checkout is re-runnable; matches git).

Added

  • git-bridge: importer-signed git→mkit import, pull, and fork-mode export back (mkit git import|fetch|pull, mkit git export --passthrough, mkit git verify|status|format-patch; behind the same default-off git-bridge feature; #330). docs/specs/SPEC-GIT-IMPORT.md pins the inbound mapping: a git upstream imports as a downstream fork whose every commit/tag is signed by a dedicated, per-state-dir-pinned import key (per-key deterministic — same key plus upstream ⇒ same hashes anywhere), original authorship preserved in the author identity, original git bytes retained for audit, and a git-import/v1 attestation minted per head. mkit git pull fast-forwards or refuses with a native mkit merge upstream/<branch> hint; force-pushes warn and rewind tracking refs only; deletions prune like git fetch --prune. Passthrough (fork mode, SPEC-GIT-BRIDGE §14) re-emits imported objects as their original sha1s so an imported repo publishes as a TRUE git fork (shared merge bases, PR-able), with an origin guard refusing disconnected re-translations toward any recorded import source. mkit git verify [--fork-audit] audits bridge state offline; format-patch renders native commits as git am-able patches. Journeys in docs/GUIDE-GIT-WORKFLOWS.md; hostile-upstream surface in THREAT-MODEL §3.1a. Closes the importer-signed-import scope of the git-interop exploration.
  • git-bridge: deterministic one-way export to git mirrors (mkit git export, behind the default-off git-bridge feature; #330). New normative spec docs/specs/SPEC-GIT-BRIDGE.md pins a byte-deterministic mkit→git object mapping (BLAKE3/SHA-1 translation with mkit-only fields — signer, signature, identity, annotation slots — carried in mkit-* commit/tag headers), so the original signed mkit objects are reconstructible bit-exactly from a mirror and their Ed25519 signatures re-verify (shallow and deep verification modes are specified). New mkit-git-bridge crate implements the mapping with golden vectors under rust/tests/golden/git-bridge/, round-trip plus determinism plus differential-vs-real-git tests (git hash-object id agreement, git fsck --strict). The exporter pushes with per-ref --force-with-lease from rebuildable state under .mkit/git/, skips untranslatable refs loudly (remix ancestry, git-illegal names, non-canonical chunking); the import direction is specified separately in docs/specs/SPEC-GIT-IMPORT.md. PARITY.md gains a scope amendment per its own renegotiation rule. Closes the export-bridge foundation scope of the git-interop exploration.
  • mkit mcp — a local Model Context Protocol server in the CLI. mkit mcp [--repository <path>] speaks newline-delimited JSON-RPC over stdio so LLM agents can drive local repositories through structured tool calls: the git-parity set (status, diffs, log, show, branch, add, unstage, signed commit, branch create/checkout, init) plus mkit's differentiators (keygen, verify, attest, verify-attest, cat-file inspection). Conservative by design: no network operations, no history surgery, never passes -f; --repository confines all calls; attestation predicate files must resolve inside the repo, trust-roots files outside it; --signer/--algorithm are always pinned explicitly so user config cannot reroute agent-triggered signing. See docs/CLI.md §"Agent integration".
  • PinResponse.pin is debug_redact ([signer.proto]): generated Debug impls print [REDACTED] in place of the PIN, so a stray {:?} log cannot leak it. Wire format and JSON unchanged.
  • Explicit decode caps on every frame decode. New mkit_rpc::frame_decode_options() applies a 16-deep recursion limit and the 1 MiB MAX_FRAME_BYTES size cap at the protobuf decode itself (buffa DecodeOptions), in read_frame and in the encrypted transport's record decodes — so the bound holds even on paths where the cipher layer, not the length prefix, does the framing.
  • rpc_decode fuzz target: SignerFrame / SshFrame wire decode never panics on arbitrary bytes, plus an Arbitrary-driven owned encode/decode roundtrip property (new opt-in arbitrary feature on mkit-rpc, from buffa generate_arbitrary codegen). Adopts commonware-invariants minifuzz for the rpc_decode target.
  • mkit Agent Skill plus crates/docs MCP server — a published Agent Skill (SKILL.md) teaching agents to drive the mkit CLI, plus a release-gated documentation MCP server indexing the source, SPEC docs, and CLI reference at each pinned release (#329, #331, #337).
  • samply profiling support for the benchmark/profiling workflow (#347).

Changed

  • Idiomatic buffa enum handling across all crates: raw EnumValue::to_i32 / as i32 comparisons replaced with EnumValue's direct PartialEq against enum values, and SHOUTY proto value names replaced with the UpperCamelCase aliases buffa 0.7 generates (ErrorCode::KeyNotFound, RefExpectation::Any, …). No behavior change; wire-value pins remain asserted numerically.
  • buffa 0.6 → 0.7.1 across all crates (mkit-rpc, mkit-attest, mkit-cli, mkit-transport-ssh, mkit-transport-enc, and the contrib/signers reference binaries), with the vendored mkit-rpc codegen regenerated under the 0.7.1 toolchain. The wire format and all existing generated APIs are unchanged; regeneration adds the new *OwnedView wrapper types, HasMessageView impls, and idiomatic UpperCamelCase enum value aliases. The declared requirement is 0.7.1 (not 0.7) because regenerated packed-view decoders call the RepeatedView::reserve hook introduced in 0.7.1.
  • commonware 2026.5.0 dependency train across the workspace, including the MMR / authenticated-bitmap port to the new APIs. No wire-format or object change.

Fixed

  • Keystore protector mismatch now names both protectors instead of an opaque error (#326). A software key record whose data-encryption key was sealed by one protector but opened with another previously surfaced a redacted, unactionable message. A new structured Error::ProtectorMismatch { required, got } reports, for example, "software key record is sealed with the macos-keychain protector but was opened with the software protector — its encrypted data-encryption key can only be unwrapped by the protector that sealed it". Protector identifiers are surfaced (they are not sensitive) while the existing path/label redaction is preserved. The message names the software-record DEK protector, not a signing-key backend, so it does not misdirect toward key.backend or <backend>:<label> routing.
  • scripts/regen-rpc-proto.sh could silently copy stale staged sources back into generated/ instead of fresh codegen output: the default build mode fills its OUT_DIR with the same .rs file set, and the script picked whichever was freshest. Codegen mode now drops a .mkit-rpc-codegen marker that the script requires.

Security

  • New git-bridge/v1 attestation predicate (SPEC-GIT-BRIDGE §11; #330): mkit git export mints one DSSE/in-toto attestation per exported head, signed with the exporter's configured signer — subject is the mkit commit (BLAKE3) plus ref name; the predicate carries the gitCommit SHA-1 as a locator (not a proof — SHA-1 is git's naming function, never a security boundary here), the mirror URL, and schema/spec versions. Bridge attestations are distinguishable from author signatures by predicate type and keyid; they assert "this exporter translated this commit", never authorship. Published on the mirror under refs/mkit/attestations. Threat model unchanged: carried signatures verify only over reconstructed mkit bytes, and translated history that fails reconstruction fails closed.
  • Advisory triage: RUSTSEC-2025-0055 (tracing-subscriber 0.2 ANSI-escape logging) is ignored in deny.toml and the audit workflow — it reaches the tree only transitively via commonware-runtime → arkworks (ark-r1cs-std) → tracing-subscriber 0.2, is arkworks-internal logging not on mkit's logging path, and has no reachable fix. Flagged for re-justification by 2026-08-21.

0.2.0 - 2026-06-10

Added

  • Annotated and signed tags (mkit tag -a / -s / -m, #230). Adds a new storable object type tag (object_type = 0x07) carrying the tagged object's hash plus type, the tagger identity, a message, a timestamp, the signer public key, and a 64-byte signature. -a creates an unsigned annotated tag; -s creates a signed tag whose signature is Ed25519 over the canonical tag bytes under a new, distinct signing domain mkit.tag\0 (deliberately separate from the commit/remix domains to prevent cross-protocol signature reuse). Lightweight mkit tag <name> is unchanged. mkit verify <rev> now verifies signed tags (resolving a tag name to its tag object), and mkit cat surfaces annotated-tag metadata. The new object type is an additive allocation within object schema v1 — no existing object layout, signing bytes, hash, or golden vector changes. New golden vectors are pinned under rust/tests/golden/tags/. Specs: docs/specs/SPEC-OBJECTS.md §6a, docs/specs/SPEC-SIGNING.md §4a.
  • mkit-keystore crate — pluggable signing-key vault subsystem (PR #109, hardened in #135 and a long tail of review-feedback follow-ups). Ships with backends for software (encrypted-at-rest, the foundation backend), software-raw, Linux Secret Service, macOS Keychain, Windows Credential Store, systemd-creds, and YubiKey (PIV and OpenPGP applets). Public interface and threat model are documented in docs/specs/SPEC-KEYSTORE.md.
  • mkit key … subcommand family — generate, list, import, export, and delete against any built-in keystore backend, with --backend/--label/--algorithm selectors and a --json output mode on list.
  • <backend>:<label> key-reference routing — commit signing, attestation signing, and the mkit key … commands resolve their signing key through user-scoped key.default_ref, key.ed25519_ref, key.secp256k1_ref, and key.p256_ref selectors. Repo-local config cannot override these for security reasons; the selector keys are accepted from $XDG_CONFIG_HOME/mkit/config and explicit flags only.
  • mkit-rpc crate — shared length-prefixed framing and wire schemas (signer.proto, common.proto) used by the external signer subprocess protocol and reserved for future agent protocols. See docs/specs/SPEC-RPC.md.
  • mkit status --porcelain=v1 — machine-readable status output matching the git status --porcelain=v1 shape, plus the mkit- specific T (mode change) status letter as the only extension.
  • mkit log --format=json — JSONL output (one commit per line) with hash, parents, tree, author, timestamp, title, and message.
  • --format=json on blame, branch, remote, config — machine-readable output across the remaining read-style commands.
  • mkit commit -a / -am <msg> — Git-style "stage tracked modifications and tracked deletions before committing" shortcut.
  • Criterion-based benchmark suite under rust/benches/ with a render-charts binary emitting buffa-style SVG charts; powers the Performance section of the README.
  • CLI port to clap-derive — every subcommand is now parsed by a derive-based parser routed through a sysexits-aware shim in mkit-cli/src/clap_shim.rs, replacing the prior hand-rolled parsers.
  • Cooperative SIGINT/SIGTERM shutdown (#111) — long-running operations poll a graceful-shutdown flag set by signal-hook and exit with tempfail (75) at natural checkpoints.
  • Writing style guide at docs/STYLE-GUIDE.md (#127).

Changed

  • Keystore capabilities now report structural operation support. Operation booleans match the corresponding Keystore operation accessors and no longer promise that the current session, daemon, hardware token, or protector is available at probe time. Operations still fail closed when runtime support is unavailable.
  • mkit commit now reads the staging index (.mkit/index) instead of recursively snapshotting the worktree. (#102) Pre-fix, mkit add and mkit rm wrote to the index but mkit commit ignored it — a half-state that surprised any user reasoning by analogy from git. Post-fix, mkit add (or mkit add .) is required before mkit commit; an empty index is now a hard error. The "snapshot the whole worktree" workflow is mkit add . && mkit commit -m "...".

    New helper: mkit_core::worktree::build_tree_from_index. Pinned invariant: for a worktree whose contents match an index entry-for- entry, build_tree and build_tree_from_index produce the same root tree hash, so attestations signed under either path cross-verify against trees built under the other.

  • Confirmation prose and progress lines move to stderr across 17 commands; stdout is reserved for machine output so `mkit status

    /tmp/out` in a clean tree produces an empty file.

Fixed

  • Keystore vault follow-up hardening (#135) — protector AAD binding, length-prefixed encrypted-record AAD, authenticated software metadata, zeroizing transient secret buffers, software metadata authentication, no-clobber imports, PIV-only YubiKey support, runtime-availability honesty in capability reports, and other review-feedback items collected across 946975e, 524d3fc, and a5b382c.
  • Silent failure exits in several subcommands now return proper sysexits-aware codes instead of exiting 1 with no diagnostic.
  • mkit commit index follow-ups — preserve executable modes on -a/-am, stage tracked deletions on add ., clear stale index path conflicts, and keep the index aligned with committed trees after PR #103 review.
  • mkit rebase preflights its signing key so the operation fails early instead of midway through a replay when no key is configured.
  • Benchmark chart axes are now apples-to-apples wallclock plus ops/s across the criterion and git2/git-CLI comparison rows.

0.1.0 - 2026-05-07

Initial public release. mkit is a content-addressed VCS for creative work with native cryptographic attestations. Earlier development tags (v0.1.0, v0.2.0, v0.2.1 from the pre-release iteration) are superseded by this release; the repository history was flattened prior to publication.

Added

  • mkit-core — content-addressed object model (BLAKE3 hashing, canonical objects, refs, packs), FastCDC chunker, delta encoding, Bao verified streaming, Ed25519 commit signing.
  • mkit-attest — DSSE plus in-toto v1 attestations with multi-algorithm signers (Ed25519, secp256k1, P-256) and an RFC 8785 JCS encoder.
  • mkit-cli — the mkit binary, with subcommands for init, add, commit, log, status, branch, checkout, merge, cherry-pick, rebase, push, pull, fetch, clone, attest, verify-attest, keygen, config.
  • Transports — memory (test), file (local), http (REST plus rustls), s3 (SigV4 over rustls, R2-compatible), ssh (forced-command server pattern over ssh(1)).
  • mkit-wasm — wasm-bindgen surface for browsers and Cloudflare Workers, published to npm as @makechain/mkit-wasm.
  • External signers — reference implementations under contrib/ for FIDO2/WebAuthn (CTAP-HID), TPM 2.0 P-256, and a raw-key file signer for development.
  • Release pipeline — cosign keyless OIDC signing, CycloneDX SBOMs, reproducible-build smoke tests, MSRV checks on Linux plus macOS.

Security

  • Per-repo .mkit/config is partitioned: security-sensitive keys (signing key paths, external-signer paths, SSH trust knobs) are user-scoped only. A hostile clone cannot redirect signing or weaken transport trust via repo-local config.
  • mkit verify-attest defaults to $XDG_CONFIG_HOME/mkit/trust-roots.toml rather than a repo-local path; in-repo trust-roots require an explicit --trust-roots flag.
  • Key files are opened with O_NOFOLLOW, written via tmp plus fsync plus rename plus parent fsync, owner-checked against the running euid, and parent directory mode is enforced 0700.
  • HTTP and S3 transports require an explicit user-scoped trusted_remote_endpoint before they will use ambient environment credentials for repo-configured remotes.
  • Reference external signer keeps secret material in a zeroizing buffer until the per-algorithm signer consumes it.

Files