CHANGELOG.md
At 26cf72c
Preview
Text · 383.2 KiBChangelog
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-rpcchange; consumers must adopt the correspondingmkit-rpcrelease. 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 encodeAnycan override the new providedTransport::atomic_force_head_conditionmethod; source adapters forward it. - Add independent release plumbing for
mkit-primitives: its own crate changelog, version and train-exclusion guard, a signed-tagmkit-primitives-vX.Y.Zpublish 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_clearancetomkit-primitives. It recomputes the contiguous published prefix from host-supplied facts and never settles an acceptance reservation or records a second terminal outcome. Adddecide_accept_and_clear, which returns the unchanged ticketed-advance decision and, only when that decision applies, a clearance over the same repository and operation, plusClearanceAdvance::with_pair, which takes the dependency and external-supplier sets from an established resulting-pair coverage. The reference prefix walk usesdecide_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
(
PackDecodeCursorreports a frame's cap when it reaches the frame and gainsresume_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 areunavailableon 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 asUnavailableInput, and reports an external depth overflow asExternalDeltaChainTooDeep(a chain mixing in-pack and external links is reported by its direct base).mkit-primitivesaddsFailureCode::persistence,classify_membership_missandFailureCode::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::ComputeConfigadds a raw-entry dispatch threshold and becomes non-exhaustive. Migration: replace struct literals withComputeConfig::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-primitivesdecision 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,ForkPairContextandForkVisibility. 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 withT::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)andForkCompletionFactsstart with no saved accepted result, so the decision refuses until the host sets it andForkCompletionFacts::new(advance, expected_result, cursor, part, packs, digest, barrier_owner). Output structs a host only reads or destructures (for exampleAcceptanceEffectsand 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-featureshas 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_missingfor the five removedGuarded*types,struct_with_pub_fields_changed_typeforRequiredSourceFence, andtype_mismatched_generic_lifetimesforCombinedAcceptance). 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, typedRequiredGuards.GuardedFork,GuardedForkAdvance,GuardedForkActivation,GuardedForkCompletionandGuardedClearanceare removed, with their borrowedrequired_guards()snapshots,matches,valid_through_ms,evaluated_at_msandsource_fence. Migration: take the decision fromGuarded::decision()(a reference; dereference theCopyenums), lowerrequired_guards()(newRequiredGuard::Row,GuardRowandGuardKind::{Fork, Clearance}; handle them in exhaustive consumers, and treat an unknown guard as a failure, never a drop), read the final source proof withfork::fork_source_proof(nowRequiredSourceFence::Fork), and compare withdecision::guards_holdagainst the live guards from each facts type's newguards()method. A preparingdecide_fork_advancethat advances carries the construction expiry as aValidThroughguard, andForkClearanceProofhas a new requireddenial_revisionthat activation guards.RefRequirements::sourcetakesPublishedSourceFence(the formerRequiredSourceFencestruct), andRequiredSourceFenceis now a non-exhaustive enum, andCombinedAcceptanceno longer takes a lifetime parameter.ForkPlangains acommitment(). The reference server's publication prefix now passes the real row revisions todecide_clearanceand 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.VerificationVerdictgains a lifetime and itsSatisfiedvariant is replaced byVerified(&[AcceptanceBasis]): pass one basis per accepted pack, built withAcceptanceBasis::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 sealedExternalBasescommitment,VerifiedPack::external_bases()),from_evidence(decodedAcceptanceEvidence) orattested_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, soRequiredGuardgainsMembershipWitness,DenialVersion,DependencyPublicationandBindingandGuardKindgainsBinding;DependencyPublicationnames its object. A ticket whose pack (identity and length) no basis covers isInvalidResponse;HostAttestation::packsnames 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.Snapshotgainsbasis; fill it with the values your store currently yields (not a copy of the required guards), or leave it empty.ClosureWalker::newtakes&VerifiedPackafter the view and refuses a view or ruleset that differs from it.Guarded::into_loweringreturns the owned parts. The newsynthetic-artifactsfeature 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::RetryReasonaddsPackmapChangedfor a force re-plan whose transfer exceeds one advance. Migration: handle that variant when matching retry reasons exhaustively.mkit_sync::PushErrorand the CLI'sDispatchErroraddForceCapabilityUnknown: 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_generationis removed andTicketFact::authority_generationisOption<Revision>: request ticket fencing withwrite.required.authority, supply the authority observation inSnapshot::authority, and convert a numeric generation with the sameRevisionencoding on both sides; the decision now emits the Authority guard. (2)ReplayFactgainsscope,AuthorityFactandGrantFactgainnamespace; a wrongly scoped observation isInvalidResponseandguards_matchcompares the key. (3)RetryReasongainsAdmittedRefChanged(wasChangedFacts) andMembershipNotVisible(wasUnavailable); map the first to a ref conflict and the second to a retryableunavailable. (4)ForkRefusal::LifecycleUnavailableis removed; activation and completion returnWithhold(ForkWithheld::LifecycleUnavailable)(a new variant ofForkActivationDecisionandForkCompletionDecision: 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 retryableUnavailableInput(wasUnsupportedCapability). 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_cleanupreturnsInvalidResponseunless each aborted reservation is stillTicketedfor its claim, and guards every ticket row up to the first failure.decide_reservation_settlementof aTicketedreservation 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 returnsInvalidResponse. A published-source fence answers an unpublished or unpaired source withnot_found"source not found", and a destination pair that differs from the captured pair with a retry.required_guard_kindsno longer demands a Membership guard for a sealed decision's own pack-membership additions. Migration: lower the newTicket,StorageandAuthorityguards 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
DecodeBudgetoutcome, as inline already answered. Migration: none for callers; a storedRejectedrow with that message is no longer written.ClosureWalkerreports 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 finalstep, after anyMissingverdict would have been reported.PackDecodeCursor::newno longer returns an entry-geometry cap;resumereports it when it reaches that frame (migration: handle the cap fromresume).FailureCodegainsExternalDeltaChainTooDeep; 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_bytesbytes 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 newStep::ReleasePage(PageCommitment)tells the host a page it holds is unreachable from the roots and from the latest checkpoint (no reply).VerifyLimits::evidence_pagesandevidence_bytesandResourceUsage::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: handleStep::ReleasePageby dropping the page (never ask for it again); aftercheckpoint()refuses withCheckpointNotReady, keep stepping (serving retentions and fetches) until the driver returnsProgressat a drained entry boundary, then callcheckpoint()once more before the next mutating call, and persist the result before stepping on. Hosts that never resume useVerifyDriver::new_with_retention(.., PageRetention::Ephemeral), which refuses checkpoints withUnsupportedCapabilityand keeps final pages out of the host. Cached pages count againstresident_bytesat their actual sizes, up topage_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 tightresident_bytesshould lowerresident_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 andretention_countnow 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_bytesasPackfileTooLargeand above the caller's ownVerifyLimits::decoded_bytesasResourceLimit; both werePackfileTooLarge. Migration: treatResourceLimitas 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 withInvalidEvidence. No wire or stored-format change; no store reset. - [embedder: breaking API]
AcceptanceEvidence::from_trusted_hostis removed; it was byte-for-bytedecode, and its name implied a trust step that does not exist. Migration: callAcceptanceEvidence::decodewith 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.authoritywithauthority_generation: Option<u64>, replaceuploadwithupload_session: Option<Vec<u8>>, and supply the reservation-derivedticketidentity. ReplaceReservationFact::Pending.operation/expires_mswithcreated_ms,reconcile_ms, andop: ReservationOp; retain operation identity in the request. Addstorage: NonetoSnapshotand an emptystoragevector toAcceptanceEffectswhen counting is disabled. HandleGuardKind::StorageandReservationTerminalKind::StorageChangedin 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::integrityprogress. Migration: useVerifyJobV1::new, or initializeintegrity: Nonein 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_entriesand replacesVerifiedPack::external_dependencies()withdependencies(). Migration: initialize the new count in struct literals and read typedDependencyRecordvalues 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-zstdnow enforces the same 8 MiB effective window and RFC 8878 block maximum aspack-ruzstd. Previously accepted oversized windows or blocks returnZstdDecompress. 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, andasync-inspection, with feature-gated publication and removal-race cases. Test-control messages add typed publication and reliance observations. Migration: exhaustiveFeatureand 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
RepoIndexpartitions, matching the existing shardedForkRepodeployment 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
ForkEnvandForkTimerwithwith_publication_policywhen using a custom pipeline policy. ImplementPublicationPolicy::fork_versionwith 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)andBatch::incrementatomically 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
NotApplicablefrom declaredSkip. 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: enablestorage-spiwhen invoking the storage macros.Profile::server_clockaccepts the independentwire::TestClocktrait; referenceManualClockremains supported whenstorage-spiis 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::ObjectSinkrequiresreuse_object: refresh the cached object's retention time and return its immediate history references, refusing missing objects. Implement this method in custom sinks; reuse aDepthMemoacross 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 defensivePackMissingcleanup 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-primitivespack 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
VerifyDriververification tomkit-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 untilStep::Completeseals aVerifiedPack. 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::PairWalkeris a sans-IO pull machine over packmap-chain acquisition, reachable-object traversal, membership facts, external delta-base suppliers, and the fork cleared set.pair_requirementdecides the coverage obligation,PairLimitssupplies every bound,PairWalker::checkpoint/resumecarry local resumable progress under a corruption-check format, andPairCoverageis a sealed artifact. Denial, inspection, publication state, and authorization stay separate host guards. AddsFailureCode::PackmapCycle. Additive ALPHA API; no public API break; no wire change. WindowCursor::metadatareturns 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-primitivesand 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-primitivesALPHA 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
runfor the wire and server-control tiers, anddifffor deterministic protocol decisions against an embedded native reference host. Versioned profile files declare features and controls; JSON reports and documented exit codes support CI. Enablereference-runnerwhen 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_visibilityreads 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 returnsNonewhen no declaration exists and never substitutes the configured default; its opaque revision brackets the declaration likerepository_visibility's.StoreError::MalformedReplymarks 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::packsalready includes confirmed pack lengths inbytes. The opt-inmkit-server/test-supportfeature supplies typed test seeders; embedders' tests should use it instead ofadapter_spicodecs. APIs are additive. No wire or stored-format change; no wipe. - Opt-in
perf-metricsbuilds emit onefork_stepevent per fork slice and onefork_step_summaryper 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 probefork.driver_probeforks a fresh source over the__test-faultsWorker route at configurable file counts (MKIT_FORK_PROBE_FILES, default500,3000). Default builds are unchanged. No wire or stored-format change; no wipe.
Fixed
- The native HTTP channel (
mkit-syncconnect-native, and somkit-clientover 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 mismatchrejection 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_argumenton multi,unauthenticatedon 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
unavailablewithstorage 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
xtermexport. - 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, retainingunavailableand HTTP 503. Error message strings change:fork publish contendedbecomesfork publish racedfor unresolved races orfork publish deadlinefor expired apply windows;fork copy contendedbecomesfork copy deadline. No API, wire schema, or stored-format change; no drain or store reset is required. ReadAccess::recover_completed_forknow 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-ruzstdselects the MIT-licensedmkit-ruzstdpackage through a versioned path dependency. Published consumers inherit the bounded decoder. Embedders can remove their workspaceruzstdpatch 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)andwith_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_configandForkTimer::from_configconstructors share fork proof settings withPipelineConfig; 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 retryableunavailable, andinvalid_argumentis unchanged. Callers that retried everyunavailablenow seeinternalonly when stored data itself is corrupt: treatinternalas permanent and keep retryingunavailableonly. 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 newCopyLane), andpartandafterno 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 aForkJobV1literal addslanes: Vec::new(); reading jobs throughdecode_jobneeds 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::ForkPlanandfork::start_planned(start_withandplan_bytesare 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
NotFoundor 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_forknow 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_ANONYMOUSdefaults 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+httpsandmkit+httppush, fetch, pull, and clone route throughClientSourceandmkit-client.mkit-transport-connectretains 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::ClientSourcesupports native push throughmkit-clientper-RPC writes. Caller-owned request policy supplies grants and admission credentials;WriteRecoveryowns helper execution and scheduling. SharedClientSessionretains upload commitments across borrowed clients and refuses reuse at another destination or signer. Channels can distinguish transient I/O from terminal refusals through additivesend_classified/ChannelErrorAPIs. Empty uploads include the explicit final chunk used by the existing transport. SharedPartReceiptStoretypes 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
PendingVerificationanswer now carries the scheduled job's actual timer wake in milliseconds for every phase of a tiny job (the existing SP2VerifyJobV1::tinybound), with the 100 ms floor and the existing seconds cap retained — previously sub-second hints applied only inAwaitDelivery. 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-metricsstorage 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_denialis 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:pushreturns a typedPushReportorPushError, usesRemoteSource, and reports progress throughSyncObserver. 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
AdvanceRefsverification hold. Workers configureADVANCE_REFS_HOLD_MSfrom 0 through 2,000; core embedders inject a timer and absolute request deadline withwith_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, andmkit-transport-connectshare 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 returnedError::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_msnow 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::ReadAccessqueries for repository visibility, published branch pairs, member locations and denial, validated inventory, bounded canonical reads, fork status, and retained completion recovery.Pipeline::read_accessborrows 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, andjust worktree-gcrecipes 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. NativeHttpexposes independentwith_metadata_timeoutandwith_transfer_timeoutbudgets andsend_typedfor native errors, includingNativeHttpError::UnsupportedAcceptEncoding.with_timeoutsets both budgets.ConnectTransport::list_refs_with_page_limitaccepts 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.MembershipNotVisibleConnect error detail (an empty message) on the repository-membership-lagunavailable, emitted at every site that answersrepository 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 noRetry-Afteris added.mkit-clientandmkit-transport-connectclassify by the detail first and keep the exact message match as a documented fallback for older servers.mkit-servergainsServerError::membership_not_visible()andMEMBERSHIP_NOT_VISIBLE_TYPE. The proto change is additive (buf breakingclean). No stored-format change and no store reset. mkit-client: host policy seams (additive API).Signer::sign_operation(defaults tosign) receives aSigningRequest(procedure, audience, repository, parsedContentCommitment, attempt) and may refuse withSignError::Refused;Observer::before_dispatch, installed withClient::with_observerorPush::run_with, runs before every write RPC with aWriteIntent(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_envelopebuilds signed auth v2 headers for a host-dispatched procedure through the same pipeline. NewErrorvariants:Vetoed,SignerRefused,ReservedHeader,EnvelopeExpired. An envelope whose window has ended is never sent.Client::advance_refsreturnsError::Invalidbefore 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-syncgains amkit-clientbackend (clientfeature):ClientSourcereadsmkit+httpsandmkit+httpremotes throughmkit_client::Client, anonymous or signed by an injectedSignerandClock, driven by anExecutor. Theconnect-nativefeature addsNativeHttp,NativeClockanddefault_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-clientgainsClient::set_repo_visibility,Visibility, andDestination::{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 earliermkit-push(never published) is renamed to it, with its code unchanged in behaviour (Push,Plan,HttpTransport,Signer,Clock). New:Clientwith a typed call per RPC (server_info,list_refs/list_all_refs,read_ref,pack_exists,download_packwith a caller size cap,update_ref/delete_ref,begin_upload,upload_pack,upload_part,complete_upload,advance_refs), anonymous or signed reads, andresolve_pack_chain. Per-RPC calls never wait: a pending verification, membership lag,Retry-Afterhint or admission challenge returns as a typedError(PendingVerification,MembershipNotVisible,RetryAfter,PaymentRequiredwith every challenge, also from a bare 402's headers). Every signed call takes an optional caller-ownedIdentity(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.unpackdecodes a pack into anObjectSink(no filesystem) and verifies commit, remix and tag signatures by default;Verification::Skipis the explicit opt-out.Push::runkeeps its waits; they now live only in its own polling loop. A golden test pins pack and packmap node ids for a fixed plan. Thepack-ruzstdfeature (default) reads compressed packs without a C toolchain;pack-zstdadds the native writer.mkit-sync: a native clone, fetch and pull library for.mkitrepositories. It owns the filesystem layer that used to live insidemkit-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 aRemoteSourcetrait whose first backend wraps mkit-core's synchronousTransport, so every existing scheme keeps working.mkit clone,fetchandpullare 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 formdispatch_embedded::<P, _>over the newEmbeddedProceduretrait) takes the request headers and the canonical body and authenticates asProcedure::Fork(auth v2, body commitment), runs anEmbeddedGate(the embedder's deployment check, then an optional pre-dispatch policy) andfork_repo, returning the typedForkResultor the pipeline'sServerErrorwith its status,Retry-Afterand 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-onlyPipeline::completed_fork, which answers a witness and never a result. The Workers adapter addsembedded::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 needstore::adapter_spi::{keys, codec},fork::decode_joborreplay::classifyto recognise a completed fork. An embedder can sign the fork request withmkit-client'sClient::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), optionalexpected_tip(field 12), andPipeline::advance_refs_from_published{,_with_meta}. Both destination names are validated before hooks or writes. Both refs useMissing; 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_sourceexposes the captured ref, pair, generation and published sequence to hooks; remote hookOperationgains 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_updatesandpipeline::{ResolvedRefUpdate, RefEffect}. Each ref has its name, new target or deletion, andCreate,Replace,Delete, orNoChange(deletion of an absent ref). Original request CAS conditions remain inOperation; 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 asCreate, with their captured targets. Fork creates membership without refs and reports no effects. The remote-hook schema addsAdmitRequest.resolved_ref_updates(field 9). Migration: configured custom Admission now runs on ticketedAdvanceRefsand implicit consumingUpdateRef, 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.ForkTimeris the kind-16 handler;fork::startandfork::stepdrive a job. Pipeline::fork_repo(SPEC-SERVER §9.9): the embedder-facing fork. A signedForkRequest(source, branch, requiredexpected_tip, destination visibility; its canonical body is the signedbody:commitment, procedure/mkit.server.v1/ForkRepo) is authorized by the caller's read of the source (every refusal is the uniformnot_foundsource 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 seesAdmissionInput::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 theCommittedoutcome and the replay record. The destination must not exist beforehand: the fork registers it. The remote hook schema gainsOperation.fork(field 9) andAdmitRequest.fork(field 8), additive. On Workers,mkit_server_worker::fork::with_fork_timerregisters kind 16 on a Paid deployment's namespace coordinators; an embedder that exposesfork_repoMUST give the timer the sametakedown_denialand 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::Quotaandfork::Failure::OverAdmitted.OpKind::ForkRepogainssource_visibility_revision; a job row written by an earlier build withoutSettleV1::declared_bytesdoes not decode (pre-release rows only).- All-parent history continuations in canonical timestamp order. An embedder
drives page 1 with per-commit
read_canonical_inon a selected-ref reader and session, feedingmkit_core::history_order::TimestampDiscovery, thenObjectReader::issue_history_continuation_inmints a version-2 binary continuation from session memo evidence and live fences alone — no object or storage proof calls.walk_history_page_with_options_inwithContinuedHistoryOrder::TimestampDiscoveryredeems 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 typedHistoryStateLimiterror in both public and owner views.walk_history_page_infirst-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
abortedbackoff 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 = authorityand WorkerNAMESPACE_MODE=authorityaccept opaque lowercase namespace names, including UUIDv7 names. Startup requires Multi addressing, an Authority hook, authority fencing, andany. 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::CallerViewis 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,GetAuthorityGenerationandSetAuthorityGenerationno 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 withnamespace not registered. The fenced namespace-creation batch now carries theagguard, so a stale Authority generation creates nothing. The Get/Set-before-first-write change also applies to self-certifying fenced deployments under policyany. - A fenced
BeginUploadretried after a generation bump replaces its stale-generation ticket in one guarded batch instead of answeringmoveduntil expiry: the old reservation gets anAborted(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_infor 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_tokensand the optional WorkerHISTORY_TOKEN_KEYSsetting 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_refnarrows 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 immutableCaptureCheckpoint. 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_instarts a page-1 loop; unscoped sessions, non-session reads andissue_urlsare unchanged. - Server readers add
walk_history_in,locate_commit_inandread_commit_path_infor 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(formerlymkit-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-messagesexposes the same generated messages without Connect bindings or tokio. Existingtransportconsumers 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
OutcomeKindandReservationV1result, including repository storage changes;Outcome::newconstructs delivery payloads without stored rows. Types retain their non-exhaustive annotations. Pipeline::repo_storage_manyreads up to 100 repositories in one namespace with one coordinatorget_many, including per-repository owner authorization. Missing and unauthorized repositories both yieldNone. The Worker embedding exposes the method andMAX_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 protoOutcome.repo_storage_changed, field 11) through the outcome sink, andPipeline::repo_storagereads{ stored_bytes, version }in one coordinator call, authorized as an owner read. Counting is part of relay delivery to a coordinator, so everyRelayHandlercounts.ReservationV1gains aRepoStorageChangedstate, so exhaustive matches must handle it. A store created before this change has repositories without a counter and must be reset. SetRepoVisibilitynow runs the embedder'sAdmissionhook and records an outcome for theOutcomeSink, 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 byinput.op.procedure() == Procedure::SetRepoVisibility(withOpKind::SetRepoVisibility { visibility },declared_bytes = 0), andDefaultAdmissioncharges nothing for it. Remote hooks send the procedure and visibility in additive optionalOutcomefields.Pipeline::set_repo_visibility_with_metaalso returns the admission's receipt headers, which the Connect service now sets.- Server embedders can share
ReadLimitsandReaderSessionacross canonical and metadata reads to cap aggregate calls, decode work, encoded I/O and output. mkit-core::history_orderadds a pure, I/O-free timestamp/discovery-order all-parent history reducer shared by the server and embedders (TimestampDiscovery,HistoryOrder::TimestampDiscovery). Decreasing canonicalCommit.timestamp/Remix.timestampkeys 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 indocs/specs/SPEC-HISTORY-ORDER.md. Existing traversal APIs — the server's BFSHistoryMode::AllParentsand 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_totalcounts 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 loopbackmkit+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 thanroot,ed25519-…and0x…, so amkit+https://host/<uuidv7>/<name>remote clone, pulls and pushes against anamespace_mode = authoritydeployment.mkit-coregainsrepo_identity::OpaqueNamespace,RemoteNamespaceand the ownedRemoteIdentity(withMAX_OPAQUE_NAMESPACE_LEN), andmkit-server's authority validator delegates toOpaqueNamespace::parseso client and server share the one grammar.mkit-transport-connectaddsremote_identity_from_urlbeside the strictrepository_identity_from_urlandConnectTransport::remote_identity();UrlIdentityErrorgainsNamespace/Namevariants that name the offending segment and the accepted grammars. The CLI prints those clearer URL/identity errors, and a plainhttps://URL is refused with guidance pointing atmkit+https://and explaining that plain HTTP serves objects by repository path (SPEC-HTTP-OBJECTS).mkit-client's pushDestinationaccepts 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 epochandmkit visibility set --statementrefuse such a URL with an error naming SPEC-SERVER §6.2.2, while the envelope form ofvisibility setworks. 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
mvalue (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-Afterhint 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::servicecalls. 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; onlyunavailableis 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_bytesis now the declared pack size orSome(0)when the pack is already counted for the repository, forBeginUploadand 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_repocounted 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. TheRepoStorageChangedoutcome is the accounting source.- Object-reader cap hits consistently return
ResourceExhaustedwith 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
ResourceExhaustedwhen 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
unavailableinstead 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
unavailablewith the messagepublication verification capacity exhaustedwhichever call it lands on. It wasinvalid_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 answersunavailablewithpublication verification limit reachedand a counter, instead ofinvalid_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_progressgains 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_splitper 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 endsClosureCapped. 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) andwith_extract_chain_stepsare 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-framerow 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, andextract_chain_steps = 1restores 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_deadlineaccepts 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=trueto activate all sixteen repository index objects concurrently with signed-read authorization.REPO_INDEX_PREWAKE_TTL_SECONDSdefaults 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 useObjectSource. CLI behavior is unchanged.MemoryOverlayprovides 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, casesembedding.public_fixtureandembedding.multipart_file_readback, markersMKIT_EMBED_*and the fixture switchFIXTURE_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_metadatanow fails the whole batch withResourceExhaustedwhen a proven object's delta depth or external base lookup cap is hit, matchingread_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-ruzstdfeature 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-AuthorizationorPAYMENT-SIGNATUREvalue 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.authoritywithauthority_generation: Option<u64>, replaceuploadwithupload_session: Option<Vec<u8>>, and supply the reservation-derivedticketidentity. ReplaceReservationFact::Pending.operation/expires_mswithcreated_ms,reconcile_ms, andop: ReservationOp; retain operation identity in the request. Addstorage: NonetoSnapshotand an emptystoragevector toAcceptanceEffectswhen counting is disabled. HandleGuardKind::StorageandReservationTerminalKind::StorageChangedin 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::Acceptseparates 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: usestart,start_with, orstart_plannedrather than job literals, handle the new phase in exhaustive matches, and reset pre-production stores before using this format. Source changes after admission use uniformnot_foundinstead of the former visibility-change retry response. - Unstable CLI library API:
remote_dispatch::open_trustedreturnsRemoteinstead ofArc<dyn Transport>. Migration: useRemote::with_sourceto 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 aDenialCapabilityobtained throughPipeline::denial_capability()withtakedown_denial=true. Takedown services and work handlers default to denial disabled and refuse writer operations withfailed_preconditionandtakedown denial disabledbefore any write. Migration: pass the pipeline's capability throughwith_denial_capabilitytotakedown::{Service, work::Work, late_owner::LateOwner}and to raw writer calls. No wire or stored-format change; wipe: no. - [embedder: breaking API]
reads::ReadAccess::newtakes a finaltakedown_denialargument. Typed object lookup and canonical member reads use the same denial gate as pipeline reads. Migration: pass the deployment's exact mode, or usePipeline::read_access()to inherit it. No wire or stored-format change; wipe: no. - [embedder: breaking API]
indexed::job::VerifyTimernow requires atakedown_denialfield, andindexed::verify::{verify_ticketed, verify_ticketed_inspected}take atakedown_denialargument immediately afterIndexedConfig. Migration: pass the same value asPipelineConfig.takedown_denialto 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 finaltakedown_denialargument, andverify::with_verification_timerstakes the mode afterauthority_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_stepscallback now returnsmkit_sync::PushError. Migration: return typed sync/push failures from the callback; the wrapper still returnsDispatchErrorto its caller. - The unstable
mkit-clilibrary'sPushControlgains anobserverfield.PushControl,StepAuthority, and push planning now belong tomkit-sync; the CLI retains its re-exports and maps typed library errors to its existing errors. Migration: construct controls withPushControl::default(), or addobserver: &NoopObserverto struct literals. Set the observer for native sync calls; CLI compatibility wrappers retain CLI progress and cancellation. Repository callers can usemkit_sync::pushwithPushOptionsand matchPushError::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::RemoteSourceread methods now return backend-neutralSourceError;SyncError::Transportis replaced bySyncError::Source, andErrorClassifierreceives&SourceError. ContextualRepositoryNotFoundandAdvertisedPackMissingerrors gain a source cause;PackChainInvalidgains an optional source cause. The CLI library's publicDispatchErrorgainsSourceand retains the same contextual causes:RepositoryNotFoundandAdvertisedPackMissinggainsource, andPackChainInvalidgains an optionalsource. Migration: update source implementations and exhaustive error matches; adapt core transport failures withSourceError::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-exhaustiveTransportErrorgainsPendingVerification,MembershipNotVisible, andRetryAfterfor host-driven transports; existing transport retry behavior is unchanged.ClientSourcegains a defaulted observer parameter and accepts observed signed clients. Existing CLI error output and exit codes stay unchanged; no printed-text changes. NativeHttpnow defaults to 30 seconds for metadata and 300 seconds forUploadPack,UploadPartandDownloadPack, including response receipt, instead of ten minutes for every request. Migration: set the independent timeout builders, orwith_timeout, when a longer bound is required. Supplieduser-agentand gzip/identity-compatibleaccept-encodingheaders 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(Syncon native targets), allowing native dispatch futures to beSend. This coversPipeline::{dispatch_embedded, dispatch_fork}and the Workers adapter'sEmbeddedDispatch::{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). ConcreteSynccallbacks 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 tomkit-client; there is no shim. Migration: depend onmkit-clientand renamemkit_pushtomkit_client. The old names (Push,Plan,Entry,Limits,Destination,HttpTransport,Signer,Clock,Error,proto) are unchanged.Outcome,PackmapModeandErrorare now#[non_exhaustive], andErrorgainsPaymentRequired,PendingVerification,MembershipNotVisible,RetryAfter,SinkandSignature: add a wildcard arm to any exhaustivematch. An HTTP 402 (orpermission_deniedwith an admission challenge) wasError::Remote; it is nowError::PaymentRequired, and a body that is not a Connect error no longer fails asInvalid("Connect error JSON")when the status is 402.Push::runstill 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
AdvanceRefsRequestand remote-hookOperationRust 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]
SliceLimitsgainsextract_chain_steps(default 16). Migration: addextract_chain_steps: SliceLimits::default().extract_chain_stepsto struct literals, or construct withSliceLimits::default()and thewith_extract_chain_stepssetter. 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::Witnesshas a newboundary: boolfield (a twenty-byte encoding when set; the nineteen-byte encoding of every other witness is unchanged and existing rows keep decoding). Migration: addboundary: falseto anyWitness { .. }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 rowsfj(fork job) andfo(fork id sets) and timer kind 16 are additive; no store reset.Procedure::Fork,OpKind::ForkRepoandStoredProcedure::Forkare additive on non-exhaustive enums.StoredResultis exhaustive, so its newForkvariant is[embedder: breaking API]. Migration: add aFork(or wildcard) arm to anymatchonStoredResult. New public items:mkit_server::fork,takedown::inventory::{SealedFacts, sealed_facts, parent_page}andtakedown::denial::require_pack_clear_sealed.pipeline::ShardMapgains a defaultedobject_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_VERSIONis 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 explicitvrow 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_fanoutnow advertises 16 andstore::REPO_INDEX_FANOUTis new;store::INDEX_FANOUTkeeps meaning the content-shard fan-out. Migration: reset the store; there is no in-place upgrade. Per-Durable-Object scaling limits are indocs/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 newtakedown::inventory::visit_dependenciesunder the same seal check asvisit. - [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 explicitHistoryStateLimit::Provenancein 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_inmints version-2 tokens, and the newissue_history_continuation_inmints timestamp continuations from a sealedTimestampDiscoverywalk. No stored-format change. - [embedder: breaking API]
http_mount::early_object_errornow requires the deployment namespace mode, so authority-mode HTTP object routes pass the early gate. Migration: pass the deployment'sNamespaceModeas the third argument, for exampleearly_object_error(method, url, cfg.namespace_mode)withcfg: WorkerConfig. No wire or stored-format change. - [embedder: breaking API]
AuthorityKey.namespacesbecomesscope: AuthorityScope. Migration: wrap exact names inAuthorityScope::Exact, parsing each with the deployment'sNamespaceMode::namespace, and useAuthorityScope::Anyonly in authority mode.AuthorityFence::newandparsekeep the default mode; usenew_with_modeorparse_with_modefor authority-owned deployments. - [embedder: breaking API]
AuthorityStatement.namespaceand HTTPParsedUrl.repositoryuse the server's mode-awarenamespacetypes. Migration: use namespacekey()for storage routing and identityname(),namespace(), andDisplayfor selectors. Useroute::parse_with_modefor authority routes;route::parseretains 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::servicereturnsconnect::ConnectService, a protected Tower service, instead of rawconnectrpc::ConnectRpcService. Use the new type in explicit annotations andConnectService::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]
StoreErroraddsOperationLimitExceededfor 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
&ObjectStoredirectly 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&ObjectStorecalls and explicit ancestor-set hasher arguments remain valid. - [embedder: breaking API] Request reservation variants require
StoredProcedure. Pass the operation directly toReservationV1::{pending, committed, aborted, read_served};with_procedureis removed. Storage-counter events remain procedure-less, and expired upload tickets still reportBeginUpload. - [embedder: breaking API] [embedder: stored-format change]
[embedder: store reset required] Stored visibility requires the server-clock
changed_ms; the creation-time fallback andvisibility_changed_msaccessor are removed. Readchanged_msdirectly and retainlast_created_msfor 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 withschema::initialize/BOOTSTRAP;StoreMaintenance::migrateremains 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=1queries 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}andHttpObjectsConfig::{max_proof_content_bytes, max_proof_bundle_bytes}are removed. Migrate to ordinaryserve_http_objectorserve_http_object_with_runtime; embedders needing disclosure bundles build them withmkit_core::verify::build_disclosure_fromandObjectReader. 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-hiddenstore::adapter_spi. Adapter/conformance implementations migrate those imports to the SPI; embedders importReservationV1,OutcomeRef,AbortReason,PendingOp,StoredProcedureandTicketCapsdirectly fromstore. - [embedder: breaking API] Core-only
store::{read, view}and unused inspection/restore planning are removed from the public surface. UsePipeline/ObjectReaderfor verified reads and portablestore::{export_partition, import_stream}for maintenance; the existing nativefsconstructors 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,AuthorityFenceand the Workers adapter's configuration types (includingHookRoles::default). Replace literals withnew, a parser orDefault, then adjust public fields;LeaseParamsandSliceLimitsalso provide builders for common overrides. ExistingPipelineConfig::new,IndexedConfig::default,HttpObjectsConfig::defaultandWorkerConfig::from_vars/from_envremain the construction API. - [embedder: breaking API]
takedown::work::Workis non-exhaustive. Replace runtime literals withWork::new(metadata, serving, preserved, WorkConfig::new(..)); configure purge, discovery margin and acquisition profile on the non-exhaustiveWorkConfigbefore assembly. Routing, retention and clock remain explicit. - [embedder: breaking API] The duplicate HTTP admission seam (
HttpAdmission,AdmitRequest,AdmitDecision,Admitted,NoAdmissionandHttpSeams::admission) becomes crate-private. Implementpipeline::Admissionin theHookSetand enableHttpObjectsConfig::admit_reads; HTTP serving and settlement keep their existing behavior.HttpSeamsretains its constructor and other public seams. - [embedder: breaking API]
ObjectReader::object_sizesis removed. Useobject_metadata, selectinglogical_lenfor Blob payload lengths andcanonical_lenfor 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 authenticatedadmin::Engineoperations or the pipeline; no adapter or application called these helpers. - [embedder: breaking API]
downloadbecomes crate-private and unusedtakedown::ACTIVATEDis 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 migratecharge(n)tocharge_operations(n); request callers retaincharge()/charge_many(n). Local purge reservations still remain charged after parent refusal. - [embedder: breaking API] The Cargo
test-faultsfeature is renamed to internal__test-faultsin the server, adapter and dependent test hosts. Update Cargo feature lists; the wire capability and conformance script's--test-faultsflag are unchanged. - [embedder: breaking API] The 0.5.0 comparison also includes earlier unreleased API changes:
indexed::publication::Exhaustion::IndexLookupis a new exhaustion variant (handle it in matches); removedpipeline::{published, PublishedSource}andPipeline::with_published_sourceare replaced by verified stored publication reads (remove thepublished-viewfeature); removedPipelineConfig::inspection_modeis replaced by the synchronous Inspect hook (remove the field); removedPurgeDelivery::fire_with_localis replaced by constructingPurgeDeliverywith the local cache and calling its timer handler. Fault-hook types and test methods remain available only with the renamed internal__test-faultsfeature; enable it explicitly in repository tests. - [embedder: breaking API]
mkit_server::sqlmoves tomkit_server_worker::sql, and thesqlfeature is removed. Import SQL store types from the Workers adapter and removesqlfrommkit-serverfeature lists. - [embedder: breaking API]
mkit_server::telemetry::pressureandmkit_server::relay::WORKER_*move tomkit_server_worker::telemetry::pressureandmkit_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}requireStoredProcedureand are#[non_exhaustive]: use the constructors with the operation argument and match with...ReservationV1::Expired,Ticketed,OutcomeRefandAbortReasonare unchanged. - [embedder: breaking API]
Outcomegainsprocedure: Option<Procedure>andvisibility: Option<RepoVisibility>. Request outcomes identify their operation, including a reservation the crash reconciler abandons; expired tickets deriveBeginUpload. SystemRepoStorageChangedoutcomes omit the procedure. Visibility is present only for visibility-change outcomes. Migration: useOutcome::neworOutcome::from_reservationfor 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 returnsOption<&RepositoryIdentity>(Nonefor an authority-mode namespace, which names no owner). Migration: useremote_identity()for the addressed repository, orrepository()where a self-certifying identity is required. No stored-format change.
Removed
- Remove
mkit-server-worker,apps/vcs-worker, andapps/embedded-worker, along with their deployment guides, runtime probes, and build and CI wiring.mkit-serverremains the reference implementation with its existing features and wasm32 support. Native HTTP wire conformance runs through the in-processTestHost. Embedders can host the reference server through its library API. - [embedder: breaking API] The Workers-only
published-viewoptimization (ref snapshots in R2, its timer,PublishedViewConfig,fetch_configured,ns_object_configured, thePUBLISHED_SNAPSHOTSbinding) and themkit-serverpublished-viewcargo feature withpipeline::publishedandPipeline::with_published_sourceare removed. Nothing enabled them by default.timers::registry::kinds::PUBLISHED_VIEWis removed and timer kind 10 stays reserved.embedding_pipelineandpurge::LocalCacheno 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_modeandWorkerConfig::inspection_modefields 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_localand the WorkerNamespaceDeliveryhandler 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 --httpandmkit serve --listen-enc, with their companion flags, are removed.mkit servekeeps themkit+ssh://session over stdio; each removed flag exits with a usage error.mkit-transport-connectno longer exposes the server-side service (itsservercargo feature is gone); only the client transport remains.- The standalone native
mkit-serverbinary, themkit-server-nativecrate and themkit-servercontainer image are removed. The release ships themkitCLI archives only; the production deployment target is the Workers adapter. - The Workers launch profile selector
LAUNCH_PROFILEvalueunois renamedpaid-workers;unoremains accepted as an alias. - Stores written by earlier unreleased server builds are not migrated; fresh
stores are required for the new
ListReposvisibility indexes and the takedown and inspection metadata. - (client) Remove
ConnectTransport::with_atomic_advancefor the upcoming 0.5.0 release.GetServerInfois 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
SHARDINGmeansd34(wrangler.jsoncsets it). There is no migration: a database writtensingle, or written before sharding was configurable and holding data, answers 503 untilSHARDING="single"is pinned. Under Single addressing the default write quota is now counted per (signer, branch). The client skips a listed branch that a staleListRefsnames after its delete (head and packmap both absent) instead of failing withPackmapMissing(WP-1.28c). - Windows is no longer a supported build, test or release target.
mkit-keystoreremoves thebackend-windows-credentialfeature andBackendKind::WindowsCredentialManager;"windows-credential"is now an unrecognized backend name. Run mkit under WSL. See Removed. mkit-cliremoves itshttp-transportcargo feature (withmkit serve --http).mkit-core::merklereplaces its provisional inclusion-proof API with commonware-identical BMT proofs and id-based verifiers, andHistoryError::Mmris renamedHistoryError::Mmb(ancestry roots and proof bytes change). See Changed.
Added
- (spec) Indexed-mode server contract, D32 file extraction, repository-isolated
resolution, and additive
PendingVerificationdetail andmax_delta_chain_depthdiscovery field (WP-4.4). - (spec) Add bounded admission credential headers to hooks.v1
AdmitRequest(WP-3.6b). - (docs) Add an unsupported TypeScript
mppxadmission Worker reference formkit.server.hooks.v1, with delayed settlement and optional signed hooks (WP-3.14). - (core) Add
pack::rewrite_excludingandpack::Rewrittenfor 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 returnsunimplemented("not implemented yet") for these RPCs and non-default new request fields until their implementing WPs land.ListRefs.page_sizeis temporarily ignored; existing requests retain their behavior. - (spec) SPEC-SERVER v1 M3 pipeline/outcome guarantees and the signed
mkit.server.hooks.v1contract, 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).&ObjectStoreimplements it (PackReader::readis unchanged, byte for byte and in error order);NoExternalBasesis the self-contained-pack source. A source not markedVERIFIEDhas its bytes re-derived, and anything but the requested object isDeltaBaseMissing, the same error as an absent base. New store-lesspack::decode_entries_with(pack, bases, limits, sink)validates and decodes a pack in pack order, handing eachDecodedEntrytosink(DecodeReportsummarizes).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 verifyingstore::ObjectSource(a per-repository index or the global object CAS), not just the on-diskObjectStore.build_disclosureis now a thin wrapper; bundle bytes are unchanged (the disclosure golden regeneration is a zero diff). - (core)
pack-ruzstdfeature: a decode-only, pure-Rust zstd backend (ruzstd0.9, withtwox-hashfor frame checksums) that lets awasm32-unknown-unknownbuild read SPEC-PACKFILE v20x03/0x04entries 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 andread_to_endgrows the output by doubling; a 512 MiB claim measured about 1.55 GiB RSS.PackWriterstill compresses only withpack-zstd, andpack-zstddecodes when both features are on. No consumer enables it yet. Also added: C-encoded v2 fixtures inrust/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, cratemkit-core-wasm-check, injust ci-scripts), apack-ruzstdnextest run injust ci, and apack-ruzstdgraph check inscripts/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 indocs/INVARIANTS.md. - (server)
mkit-servercrate (internal foundation for the production server, production server work): repo and namespace identifiers, principals, the typedOperationmodel, a transport-neutralServerErrorwith redaction and response shaping, theMaybeSend/Clock/Spawner/send_wrapruntime model and aMetricsfacade. Builds for native andwasm32; first published with 0.5. - (server)
mkit-serverstorage contract core: the key-levelNamespaceStore(one declarativeBatchofAbsent/Present/Equalspreconditions and aNotAfterdeadline on the backend's own clock, plus puts and deletes; get/has/get_many/ordered scan), the key-layout registry, and the in-memoryMemoryKvreference backend behind thememoryfeature. No SQL required of a backend. - (server)
mkit-serverreplay-ledger model (a stored result can never be a challenge or a retryable outcome), versioned value codecs, typed readers over anyNamespaceStore, and the content-addressedBlobStore/PackSinkcontract with theMemoryBlobStorereference backend. - (server)
mkit-server-conformancecrate (publish = false): the storage-backend conformance suite, the gate for third-party backends. 74 generic cases overNamespaceStore,BlobStoreandContentIndex(preconditions, scans, limits, capabilities, theNotAfterdeadline, capacity, cancellation, crash/restart, export/import, golden encodings); a backend implementsKvHarness, declares its expected skips (any other skip fails) and invokesstorage_suite!. Contract changes:scanrejects a cursor outside the scanned range asInvalid(soListRefsrejects 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 aRefsOnlystore 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
grantsfeature (not default):mkit_attest::grantparses and encodes themkit-write-grant:v1statement 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_headmaps one to the head that covers it, §8.3), the grant id (only fromparse_with_idorGrant::id, never from caller bytes), and theX-Write-Grantheader (<statement>.<scheme>.<blob>, strict unpadded base64url). New fuzz targetgrant_parse. Golden and reject vectors underrust/tests/golden/grants, cross-checked byscripts/golden/grants_ref.py. SemVer: additive. - (attest) SPEC-WRITE-GRANTS verifier (feature
grants): themkit-write-epoch:v1andmkit-repo-visibility:v1statement codecs, the pureepoch_transition(§5.2 check 7, overflow-safe, with the retry rule), theed25519owner scheme (verify_strictover the BLAKE3 of the statement, bound toed25519-namespaces; the ECDSA schemes returnSchemeNotImplementeduntil WP-2.5), and the stateless verifier:verify_grant_owner(§7 steps 1, 3, 4, cacheable by header bytes) plusOwnerVerified::check(steps 2, 5–7, 9, 10),verify_epoch_statementandverify_visibility_statement(distinctVerifiedEpochandVerifiedVisibilityresults, valid at onenowand never to be cached), andverify_for_registration(§10). Expiry is exclusive (now < expiry), unlike auth v2.VerifierConfigholds the deployment's own audience and refuses a loopback one unless built withnew_allowing_loopback(§3.2, §10), and refuseswebauthn-p256without a relying party (§4.3). The verifier results have no public fields or constructors; aVerifiedGrant'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,v27 or 28, and the recovered address must equal the0xnamespace) andwebauthn-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 strictclientDataJSON: duplicate member names rejected at any depth, nesting at most 64 deep, finite binary64 numbers only,type, exact challenge,crossOrigin, notopOrigin, signature over the received bytes; SPEC-WRITE-GRANTS §4.3 now states the depth and number limits). The legacy DSSE helperverify_webauthn_wrapping_with_policyis documented as lax and for self-checks only.mkit-attestnow enables serde_json'sfloat_roundtripfeature (correctly rounded number parsing, so a value that rounds tof64::MAXis finite); by Cargo feature unification this applies to every serde_json parse in a build that includesmkit-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. NewRelyingParty,WebAuthnAssertionandwebauthn_challenge;OwnerVerified/VerifiedEpoch/VerifiedVisibility::relying_party. Breaking (unreleased API):VerifierConfig::newandnew_allowing_loopbacktake the relying parties (webauthn-p256needs one;newrefuses loopback ones), a cachedOwnerVerifiedre-checks its relying party incheck, andGrantError::SchemeNotImplementedis replaced by per-rule variants. Signed goldens and verify-reject vectors for both schemes, cross-checked byscripts/golden/grants_ref.pywith python-ecdsaand pycryptodome. New fuzz targetepoch_visibility_parse; signed goldens{grant,epoch,visibility}-ed25519.jsonandreject/verify-*.json, cross-checked with pycryptodome byscripts/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 throughparse_bare_allowed). SemVer: additive. - (server)
mkit-serverstreaming pipeline:Pipeline::begin_uploadreturns anUploadSession(fresh, resume or replay of the signed operation; the replay record is reservedInFlight { resumable: true }with the quota charge before any chunk, and committed by a second batch planned with its ownNotAfterdeadline after the stream), andPipeline::downloadreturns aDownloadStreamof 800 KiB chunks. Both hold at most one chunk. Resuming an in-flight upload is legacy M0 behavior for un-ticketed uploads (vcs-workerparity), 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 finalpre_receiverejection 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. Thetest-faultsfeature addsFaultHooksat five points and per-requestTestDirectives. SemVer: unreleased API. - (server)
mkit-serverfsfeature (native, std-only, no async runtime):FsBlobStorestreams uploads intopacks/<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.FsLayoutStorekeeps a repo'srefs/refs asFileTransportref files under its ref lock (.mkit/refs/.lock), evaluating aNotAfterdeadline under that lock. An undecodable ref file isCorrupt, a ref whose file clashes with another ref's directory (or the reverse) isInvalid, and a full disk or quota isFull. Both pass the storage conformance suite. SemVer: unreleased API. Behavior change:mkit servewrote a ref named outsiderefs/(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 andFileTransport, so a name likepacks/<hex>can no longer overwrite a pack. (The pipeline now refuses such names outright; see themkit-serverrefs/entry above.) - (transport-file)
FileTransport::with_ref_lockruns a closure under the ref lock with aLockedRefshandle (read_ref,update_ref,delete_ref, and atomicwrite_file/remove_fileconfined to.mkit/server/, symlink-escape guarded); typedRefFileError; strictread_ref_strict/list_refs_strict(an undecodable ref file is an error, not absent);server_path,root,temp_path,sync_dirandcreate_dir_all_durablyare 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) isInvalidRef, not aRemoteError. 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)
SQLitemetadata backend:mkit-server'ssqlfeature addsSqlKvStore, theNamespaceStorecontract implemented once over a synchronousSqlConn(onekvtable keyed by partition and key, each batch one transaction,NotAfterread on the backend clock inside it) with versioned, forward-only schema migrations that refuse a newer schema. An optionalCapacitysets a hard cap and a soft limit a reserve below it: batches with a put returnStoreError::Fullat 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 themkit-cligraph. SemVer: unreleased API. - (server) New
mkit-server-workercrate (publish = false), the storage half of the Cloudflare Workers adapter:R2BlobStorestreams both ways (a put spawned atbeginand 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 sharedSqlKvStore, batches intransactionSync, a soft cap below the 10 GB limit measured bydatabaseSize, which excludes free pages), the Durable Object request handler, andDoNamespaceStore, 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 movesrust/Cargo.lockfrom wasm-bindgen 0.2.127 to 0.2.128 (with js-sys/web-sys 0.3.105, wasm-bindgen-futures 0.4.78), whichmkit-wasmshares. SemVer: unreleased API. - (transport-s3)
sigv4::sign_request_with_payload_hashsigns a request for a body whose SHA-256 the caller already has, so a streamed upload need not hold its body in memory;sign_requestnow delegates to it, byte for byte. SemVer: additive. - (transport-enc)
serve_tcp_listener: the async accept loop on a caller-boundTcpListener, for a server already on a tokio runtime, with separate caps on handshakes and sessions (ListenerLimits) and a shutdown future; it retries a transientaccepterror (a reset connection, descriptor exhaustion) and, after shutdown, stops accepting and waits for the sessions in flight (the caller bounds the wait). The blockingserve_tcp_*entry points share its accept loop with their signatures and behavior unchanged (uncapped, stopping at the firstaccepterror and leaving sessions running). SemVer: additive. - (core) Resumable-part building blocks (SPEC-TRANSPORT-CONNECT §7.6):
write_auth::ContentCommitmentparses and formatsbody:,pack:and the newpart:<ticket>:<index>:<subtree>:<len>commitment, andverify_headers_with(.., ExpectedCommitment::PartStream, ..)verifies anUploadPartenvelope; the newupload_partsmodule validates part geometry, streams a part into its BLAKE3 subtree chaining value and merges part values into the pack id. Golden vectors underrust/tests/golden/{uploads,auth-v2}. SemVer: additive. - (attest)
mkit_attest::eth, behind the new default-offgrantsfeature: Keccak-256 (original Keccak, not SHA3-256), the EIP-191 personal-message digest, strict low-S secp256k1 recovery fromr ‖ s ‖ v, curve-validated secp256k1 and P-256 address derivation, and the SPEC-WRITE-GRANTS §4.4 client normalizers (EIP-191v/high-s, P-256 DER to raw low-Sr ‖ s), with a module-localEthError. Adds thesha3 0.11dependency (optional). Golden vectors inrust/tests/golden/grants/eth-primitives.json, cross-checked against Foundrycast, viem and pycryptodome. - (core) Closure verification now has a pull-based
ObjectSource/verify_closure_streamingAPI and a nativeverify_closure_storehelper.ClosureReportgains the additiveunreferenced_checkedfield: map and pack verification set it totrue, while streaming verification leavesunreferencedempty and sets it tofalse. SemVer: additive in this pre-release, but the new report field is a struct-literal break for external constructors. - (cli) Local
mkit closure verifynow reads only reachable objects by default;--show-unreferencedretains 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_checkedin their JSON results and use borrowed raw pack entries during verification. SemVer: additive. - (wasm)
verify_disclosureJSON includesstep_inner_roots(hex array) andchunk_inner_root(hex ornull). SemVer: additive. - (cli)
mkit verify-proof --format=jsonincludes 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, andremix_verifyexpose 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::verifymodule (issue #1015 verifier kit PR 2) anddocs/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, andverify_blob_len_proofare the small, independently usable primitives (each corresponds to one hop of commit → tree steps → leaf → chunk/slice);verify_disclosuredecodes a self-contained wire bundle (magic"MKDP",commonware-codecconventions: fixed integers big-endian, every length an LEB128 varint) and composes them; the native-onlybuild_disclosure(needsObjectStore) is the producer side, taking aSelector::{Object, Chunk, Range}. Every check is stated against an object id — never a bare BMT inner root or an unauthenticated path/offset claim (seedocs/INVARIANTS.md). AChunkedBlobchunk'stotal_size/chunk_sizeis authenticated alongside its content id in one multi-proof over BMT positions{0, index + 1}(merkle::verify_chunk_with_meta_leaf, a newpub(crate)helper — the verifier computes the metadata leaf itself, never accepting one as caller-supplied input, preservingverify_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; aChunkedBlobrange additionally supports proving the disclosed range's absolute file offset via a complete0..indexset of per-preceding-chunk length proofs. New dependency:bao = "0.13"(pure Rust, wasm32-clean — already proven bymkit-wasm's existing use of the same crate). Golden vectors:rust/tests/golden/disclosure/(10 accept, 12 reject vectors), consumed byrust/crates/mkit-core/tests/golden_disclosure.rs. New fuzz targetsdisclosure_decodeandverify_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 aspayload_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-backedreachable_snapshotjoinsreachable_objects(history).PackWriter::new_raw_onlynever compresses and rejects deltas, always finishing as v1 with only0x00entries.PackEntriesis a store-less pack iterator (PackReader::readconsumes it; existing pack goldens and thepackfuzz target are unchanged).verify_closure/verify_closure_packs/verify_closure_manifest/export_closureare wasm-safe except the nativeexport_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 targetspack_entriesandverify_closure. SemVer: additive — new APIs;PackReaderbehavior unchanged. - (cli)
mkit prove,mkit verify-proof, andmkit 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 toolsmkit_prove/mkit_verify_proof/mkit_closure_verify). Distinct frommkit 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 rawbao_*exports are unchanged for the web streaming demo), andwrap_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 atverify::MAX_BUNDLE_BYTES(64 MiB). Native tests replay theproofs/,disclosure/, andclosure/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 documentedmkit-wasmAPI) while writing it. Linked fromREADME.md,SKILL.md, anddocs/ARCHITECTURE.md(already reachable through the docs MCP's existingget_file/search_docstools). SemVer: none, docs only. - (wasm)
MAX_CLOSURE_INPUT_BYTES(1 GiB, matchingmkit_core::store::MAX_RAW_OBJECT_SIZE):verify_closure_packsandverify_closure_manifestnow cap their concatenated packs input independently ofverify::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_sizesremains 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_withnow takes explicitHookCapabilities; shared config factories usefrom_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 withdurable_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): addhttp.sslCAInfoand overridingMKIT_SSL_CA_FILEfor 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|privateon 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::MemorySourcefor 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_foundand 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
pvare 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::hooksmessage types with JSON support and runtime-freeHookSignerandHookVerifier, enabled by thehooksfeature. The server re-exports its authentication surface for external hook implementers (WP-3.7b). - Server (Stage 2, inert): default-off
http-objectsadapter 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_readsenables 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:HttpReadRuntimeinjects retained tasks and deadlines, andHttpObjectRequest::header_namespreserves 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_policywith per-ref allowed operation signers (both modes) and fast-forward-only rules (indexed mode). In indexed mode au-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_ticketedreturnsStagedCommits;VerifiedAuth::created_at_msandIndexedConfig::max_ancestry_commitsare 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_HOOKservice binding (HOOK_ROLES,HOOK_TIMEOUT_MS,AUTHORIZER_ROLE), andadapter::fetch_withandns_object_withtake a deployment's ownHookSetand outcome sink (WP-3.9). The Queue outcome sink and a signed Worker webhook are deferred (WP-3.9b). - Server (Stage 2, inert):
mkit-servergains the default-offhttp-objectsfeature:Pipeline::serve_http_objectserves 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 needsPipelineConfig::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: newProcedure::{HttpGetObject, HttpGetRefPath}andOpKind::HttpGet. Request queries useRedactedQuery, 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-faultstimer directive also expires tickets and the Worker stats hook is scoped to one ref's shard under D34.mkit-server-conformanceProfilegainsticket_per_refandmerge_paging_refs(WP-1.27). - Server: the Worker gains the
GRANT_SCHEMESandWEBAUTHN_RPSvars and atest-faults-onlyUNSAFE_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 answersPendingVerificationuntil the slices finish (Paid plan only; index rows are relayed after the decode reachesDone, andVerifiedwaits for their delivery). The job lives in the newvckey 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-coregainsWindowReader::last_frameandpack::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. NewIndexedConfig::{extract_min_bytes, max_extract_bytes}. Workers still refuse indexed mode (WP-4.10). API:IndexedConfig::max_extract_bytesis nowOption<u64>(default4 * max_pack_bytes);verify_ticketedtakes the consuming ticket ids and needs aMultipartBlobStore;ContentIndexgainsextend_holdandadd_holder_unless_blocked, andrelease_holdis deadline-guarded;is_reserved_pack_keyspaceis 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
PushTooLargeafter 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=jsonreportssteps(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-Authenticatefields (WP-3.5). - Server:
mkit serve --root <DIR>serves the repositories under<DIR>addressed by<NAMESPACE>/<NAME>(from the path argument or a strictSSH_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 — somkit+ssh://pushes need no upload tickets. Denied writes answerINVALID_REQUEST "write not permitted"(WP-1.15). - Server: the Worker adapter can serve multi-repository deployments (the
ADDRESSING/NAMESPACE_POLICY/NAMESPACE_ALLOWLIST/UNSAFE_OPEN_NAMESPACESvars). 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-hooksfeature tomkit-server: Authorize, Admit and Outcome overmkit.server.hooks.v1on a transport-agnosticHookChannel, with Ed25519-signed requests, fail-closed mapping and response bounds. Adds theSleeptimeout seam, andscripts/regen-hooks-proto.shfor the vendored codegen. - CLI: add
mkit grant create|add|listand 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 keygrant.webauthn_rp. Issue grants for pushes that move a branch ascuf, notcu: while servers are opaque, updating an existing ref needs thefflag.mkit-keystoregains a defaultedKeySigner::sign_prehash_recoverable_secp256k1for the software backends (WP-2.13). - CLI: add
mkit epoch show|bump,mkit grant revoke [--prune]andmkit visibility set, waiting out the server'sRetry-After. Connect client: newConnectTransport::{get_grant_epoch, set_grant_epoch, set_repo_visibility}returningCompletion, and a per-request signed-or-unsigned classification ofSetRepoVisibility(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
Expiredoutcome 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
ANYand 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
DownloadPackrequest, and add private repositories:SetRepoVisibilityin envelope and owner-statement modes, a coordinatorrvrow, andnot_foundfor every unauthorized private read, indistinguishable from a missing repository (WP-2.9). A read that carries auth headers but fails verification is nowunauthenticatedinstead of anonymous, and a grant header on an unsigned Multi request isunauthenticatedon every procedure. - Server: mint short-lived
mkit-url-token:v1object URL tokens withIssueObjectUrl, 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
0xnamespaces, 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).
TransportErroris 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
DownloadPackrequest. Add a grant-source API and local selection logic; the user grant store follows in WP-2.13. - Transport: add the
GetGrantEpoch,SetGrantEpoch,SetRepoVisibilityandIssueObjectUrlmessages and RPCs; the server answersunimplementeduntil WP-2.8, WP-2.9 and WP-2.11 (WP-2.2). - Transport: add the
AdmissionChallengeandChallengeerror 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:
AdvanceRefsconsumes verified upload tickets atomically with ref publication, outcomes and repository membership. Missing packs after upload close their tickets withAborted(PACK_MISSING).UpdateRefandAdvanceRefsnow support conditional ref deletion; direct writes with admission reservations fail closed until WP-3.3. - Server: add stateless
UploadPartandCompleteUpload, 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_ticketso the FS session directory uses the ticket id. - Server: ticketed
UploadPacknow 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
BACKUPSbucket, 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 privatemkit-vcs-backupsbucket bound asBACKUPSand a 35-daybackups/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.leasesand Event proto goldens (WP-5.1a-1). Server enforcement follows in M5. - Server: implement authenticated
BeginUploadtickets 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 withTICKET_KEYS. The internal legacy UploadPack session API is nowopen_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::gaugehas a provided no-op default; P-24 is nowmkit_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) ord34with 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.unoremains an accepted deprecated alias and logs a startup warning. Takedown startup accepts configured purge delivery through signed HTTPS or an embedder-suppliedPurgeSink; 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
LeasedShardV1 encoding gains watermark and sweep due fields. - (client) Validate the remote URL path as a repository identity and carry
X-Repositoryon every Connect RPC, including anonymous reads. Empty paths addressdefault; 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 pagedListRefsresponses with ordering and termination guards. - (core) Add defaulted
Transport::download_pack_via_ref,download_blob_via_ref, andpack_exists_via_refmethods. Connect uses their validatedX-Mkit-Refhint for packmap-driven downloads; other transports preserve their existing behavior. Add defaultedrepository_addressfor remote error context. - (client) Retry Connect
abortedresponses as temporary failures (503). Missing ref reads returnNone; missing pack checks returnfalse. - (server) Scope Multi
PackExistsandDownloadPackto repository membership, with the optional unsignedX-Mkit-Refhint 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
rhdeduplication watermarks, atomic pre-delivery hooks, and bounded source cleanup (WP-1.23a). Relay writers now callrelay_at(now_ms)to stamp rows and commit their kick timer;RelayV1gains mandatoryat_msin 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
GetServerInfodeployment 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_deniedbefore 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, andanywith 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 atDone. 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 withPackEntries; allocation failures returnPackfileTooLarge. - (server) Validate
X-Repositorybefore authentication and storage access. Single deployments retain headerless reads and signed writes naming their configured identity; another valid identity returnsnot_found, and a malformed identity returnsinvalid_argument. WorkerAUTH_REPOSITORYnow require the SPEC-TRANSPORT-CONNECT §7.4 grammar (previously any printable ASCII was accepted);defaultremains valid. Embedders can constructAddressing::Multito route refs and replay state by namespace and repository. Multi pack RPCs returnunimplementeduntil repository membership lands in WP-1.10.Addressing::resolvereturns aResolvedRepo, stored onAuthenticated::repo(), without changing the adapter-facing authentication API. - (cli)
mkit serve <path>runs onmkit-server: its engine ismkit_server::ssh::serve_sessionover the pipeline with the.mkitlayout stores (FsBlobStore,FsLayoutStore), driven byfutures::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-clinow depends onmkit-server(featuressshandfsonly), which is first published with the 0.5 release. Behavior changes:- Idle timeout.
mkit serveends a session after--idle-timeout-secs(default 60;0disables it) without a byte from the client, answeringError{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(default0, 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 outsiderefs/(main,heads/main) thatmkit serveused 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 outsiderefs/", gives afind/mvrecipe for an operator to locate and move them by hand. See themkit-serverentry below. - Crashed uploads are swept. At startup, when no other
mkit serveor server process holdsserve.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(injust ci-scripts; INVARIANTS "The defaultmkitCLI is server-free").
- Idle timeout.
- (server) The pipeline serves only ref names under
refs/(mkit_server::refs::is_served_ref_name; SPEC-REFS §2), on every binding:ReadRef,UpdateRefandAdvanceRefsrefuse any other grammar-valid name withinvalid_argument"ref name must start with refs/ …" (INVALID_REQUESTon the ssh wire), where they used to store it (in.mkit/server/rows/on the.mkitlayout, invisible to the CLI).ListRefsprefixes are unrestricted; one outsiderefs/lists nothing. Normative in SPEC-REFS v3 §2 (the §2 namespace list now includesrefs/mkit/packmap/), SPEC-TRANSPORT §4.2.1 and SPEC-TRANSPORT-CONNECT §5; new wire-suite caserefs.non_refs_prefix_rejected. NewFsBlobStore::sweep_stale_uploads. SemVer: unreleased API. - (transport-file) Every ref write through
FileTransport(update_ref/write_ref, andLockedRefs::update_ref,delete_ref,write_file,remove_file) refuses a root carryingSERVER_META_MARKER(.mkit/server-meta), the marker a SQLite-metadata server deployment writes, with the newRefFileError::MetaElsewhere(aTransportError::RemoteErroron theTransportverbs). That root's refs live in the server'sSQLitedatabase, so a localmkit pushto afile://remote ormkit servecan 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-workeris a thin deployment ofmkit-server-worker: its fetch handler andRefStoreDurable Object call the newmkit_server_worker::adapter, which servesmkit-server's pipeline over R2 and Durable Object SQLite, streaming request and response bodies (no whole-pack buffering;DownloadPacksends 800 KiB chunks instead of one). ClassRefStore, bindingREFSTOREand bucketSTORAGEare unchanged, with no wrangler migration; the Durable Object now keeps akvtable and ignores the oldrefs,write_quotaandauthenticated_operationstables (never deployed; no migration). Wire changes, all SPEC-mandated: a reused nonce for another operation isinvalid_argument(was an uncaught 500); a 33-byteexpected_idisinvalid_argument(wasfailed_precondition);ListRefsmatches its prefix at a path-component boundary; a gzip-compressed unary response (e.g. a largeListRefs) is no longer compressed a second time by the runtime; an upload stream that runs past its declared size and past 64 MiB isinvalid_argument(wasresource_exhausted); storage failures surface asinternal/unavailableinstead ofinvalid_argument"refstore …"; a missingAUTH_AUDIENCEorAUTH_REPOSITORYmakes every RPCunavailable(was: writes only). New varWORKERS_PLAN(free, the default, orpaid) caps the Durable Object store for the plan. The generated code,build.rsand the duplicated modules are gone;scripts/vcs-worker-conformance.shruns the wire suite against it underwrangler dev. - (core)
mkit_core::refs::validate_ref_namenow also requires a name of at mostMAX_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 newRefError::RefNameTooLong, whose message names the limit. Reading, listing, resolving and deleting a local ref that already exists check only the grammar (the newvalidate_ref_name_grammar), so a longer ref written earlier stays visible and can be deleted or renamed withmkit 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 mostMAX_TAG_NAME_BYTES(502), so their wire names (refs/heads/<b>andrefs/mkit/packmap/<b>,refs/tags/<t>) fit the 512-byte bound; the error names the derived limit and why (RefError::RefNameTooLongnow carries aRefNameKind). 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_NAMEdrops from 4096 to 512 (it is nowmkit_core::refs::MAX_REF_NAME_BYTES), so the ssh and enc clients refuse a longer name orListRefsprefix before sending.mkit-serverrefuses one onReadRef/UpdateRefwithinvalid_argument"ref name too long" (INVALID_REQUESTon the ssh wire), and aListRefsover the.mkitlayout skips a longer legacy ref file with a warning.mkit serveover ssh used to accept names of any length. Pre-production policy: no migration. - (server)
mkit-server'sListRefsmatches its prefix at a path-component boundary and strips the prefix plus its/, as SPEC-REFS §4 andmkit servedo (refs/headsandrefs/heads/listmain;refs/heads/malists nothing). A listing of the.mkitlayout skips a ref file that holds no ref id, with a warning, instead of failing.FileTransport::list_refs_strictis replaced bylist_ref_files, which reports such a file asNone. - (core) Pack readers enforce SPEC-PACKFILE §3.3's "one zstd frame"
rule. A
0x03/0x04payload holding two concatenated frames, a skippable or legacy-magic frame, or trailing bytes after the frame now fails withPackError::ZstdDecompress. The C path (zstd::bulk::decompress) used to decode concatenated frames and skip skippable ones. mkit'sPackWriternever produced such payloads. Pre-production policy: no compatibility path. - (core)
list_refs/tag/remote listing read each ref with a single stack-bufferread(open+read+close) instead offs::read's extrastatxand EOF probe: −23% at 10k refs, −25% at 100 refs (refs_opsbench). - (core) SPEC-DISCLOSURE v2: every
Stepand chunk header carries a mandatory 32-byteinner_root(bare BMT root of the parent Tree / ChunkedBlob). Bundle version byte is2; version1is a typedUnsupportedBundleVersion(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 behindunified_hunks/enumerate_hunks—diff,show,add -p, andmerge_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'sxdiffand 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 itsv/backtrack-tracestate off the wholeold.len() + new.len(), regardless of how small the actual edit was. New elision logic inmyers_changedfinds the shared prefix with one cheap linear scan, runs the renamedmyers_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 (atd = 0there 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 matchesold[0]to the firstnew"a"; backward-suffix-matching commits to the second) and confirmed by exhaustive brute-force diffing over small alphabets. Theapply_hunks_subsetround-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 newproptest_myers_changed_matches_unelided_corecomparesmyers_changed's output directly againstmyers_changed_corerun 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 counterexampleold = [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_scriptsuite (single-line edit and 200-line append against 1k/10k/100k-line files, plus ano_common_affixcontrol 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 issplit_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'sO((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 861mkit-coretests pass (including both new property tests),cargo clippy -p mkit-core -p mkit-benches --all-targets -- -D warningsandcargo fmt --checkare clean. SemVer: none —myers_changed/myers_changed_coreare private toops::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 behindmkit add/status/diffon any file aboveCHUNK_THRESHOLD, 1 MiB). The existing implementation was strictly phase-serial: cut a batch of up toSTREAM_HASH_BATCH(64) chunks on the calling thread, then block waiting for the caller'shash_chunkscallback (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-corestays wasm-clean, and wasm32 has no threads to pipeline across) now run the cutter on its ownstd::thread::scopeworker, handing batches to the calling thread over a rendezvous (mpsc::sync_channel(0)) channel — a zero-capacity channel'ssendblocks until the matchingrecv, 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 moreSTREAM_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
recvas soon ashash_chunkserrors (the natural?-propagation shape), the cutter thread's nextsend— for a batch it already cut, or is still cutting — has no buffer to land in and blocks forever waiting for arecvthat will never come, hangingstd::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 cuttersendstill 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. Newstreaming_pipeline_errors_promptly_when_hash_chunks_fails_on_an_early_batchpins 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 boundedrecv_timeoutso 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-threadhash_chunks, median of 10 runs each, to cancel this shared host's cross-process noise) at sizes spanningchunk_hash_fanout's existingfile/<n>_mibbench: 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 860mkit-coretests (up from 859) pass,cargo clippy --all-targets --all-features -- -D warningsandcargo fmt --checkare clean, and awasm32-unknown-unknownbuild ofmkit-core(--no-default-features) succeeds using the unchanged phase-serial fallback. SemVer: none —store_large_file_streaming_batched/store_large_file_streaming_pipelinedare private;store_large_file_streaming_with's public signature gains a+ Sendbound on itsR: Readreader 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 behindadd,status,diff, andmergeonce two sides' object ids already differ) gained a fast path for theChunkedBlob-vs-ChunkedBlobcase — every file aboveCHUNK_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". Newchunked_content_eqwalks 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 trustcontent_eq's pre-existinga == bwhole-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 bychunked_fully_misaligned_but_equal_content/chunked_shared_prefix_then_misaligned_equal_suffixand the pre-existinglarge_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_sizeis also checked up front without reading anything, trusting the manifest's declared size the same wayLoadedBlob::len's doc comment already documents (every reassembly path enforces it viaChunkedBlob::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, bychunked_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_eqsuite (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 (theidenticalseries, which hits the pre-existinga == bfast path before ever reachingchunked_content_eq, is unchanged, as expected). New tests: direct coverage ofchunked_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-FastCDCinsertion test that confirms at the manifest level (shared first/last chunk, differing middle) that a genuine divergence-then-resync actually happens before checkingcontent_eqagainst ground truth, andcontent_eq_real_chunked_mutations_match_ground_truth, which runscontent_eqover realFastCDC-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_eqonly ever compared the two manifests' declaredtotal_sizefields 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 wrongtotal_sizewere reported merely "unequal" instead of raisingChunkedBlobSizeMismatchthe 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 declaredtotal_sizebefore 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. Newchunked_wrong_total_size_is_detected_when_no_chunk_is_skippedandchunked_wrong_total_size_on_one_side_only_is_detectedpin the fix directly (both previously-missed error cases; both now assertChunkedBlobSizeMismatch). The review also found three call sites in this file (content_fingerprint,ContentCursor::remaining, andchunked_content_eq) had each hand-rolled the identical "read a manifest chunk, requireBlob" match; factored into one sharedread_blob_chunkhelper (read_chunk, the pre-existingWorktreeError-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. Allmkit-coretests (842, up from the base branch's 839) pass,cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean, and thecontent_eqbench numbers above are unaffected (re-measured after the fix). SemVer: none —chunked_content_eq,read_blob_chunk, andContentCursor::from_objectare private toworktree::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 defaultSipHash-1-3 hasher) with the same map keyed byrustc_hash::FxHasher, seeded perencode()call. The index's keys are already the output ofencode's own FNV-1ablock_hashover each 16-byte block ofbase, soSipHash'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 (viaBuildHasherDefault/the crate's own bareFxBuildHasher, both always starting from the same compile-time state), caught in review as a real regression —block_hashis itself unkeyed, so an attacker who controlsbase's bytes could solve for many distinct blocks whoseblock_hashoutputs 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 attackSipHashexists to prevent, reopened.encodenow seeds viarustc_hash::FxSeededState::with_seed, drawing a fresh randomusizeper call fromstd's own randomized hasher (random_seed, avoiding a newrand/getrandomdependency 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 therustc-hashcrate directly (already resolved transitively viamkit-cli'sreqwest → quinnchain, 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 bareFxHasher/BuildHasherDefaultdoesn't, so no hand-rolledBuildHasherwrapper 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 oneRandomStateconstruction perencode()call, but still a clear, real win. Newrandom_seed_differs_across_calls_and_seeds_a_usable_maptest pins this module's own contribution —FxHasher's mixing quality isrustc_hash's tested contract, not this module's to reverify — confirming tworandom_seed()calls differ (the load-bearing security property) and thatFxSeededStatewires a seed through consistently enough for aHashMapto find keys it just inserted. No wire-format or public-API change —FxIndexMap/random_seedare private todelta.rs, andencode's output is byte-for-byte identical to before (allmkit-coretests, includingdelta'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 separateverify_strictcall — one scalar-multiplication-plus-comparison per object. Newmkit_core::sign::verify_batch(behind a new, non-defaultbatch-verifyfeature) instead runsed25519_dalek::verify_batch's randomized-coefficient multiscalar-multiplication check over many signatures in one pass — strictly less total scalar-multiplication work than oneverify_strictper signature — at the cost of not learning which entry failed on a batch rejection;verify_new_object_signaturesfalls 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 ofverify_strict's malleability checks (see its README's "Malleability" section for the public-key half; the signature-Rhalf is the same class of check, just undocumented as a batch caveat) —verify_batchperforms both itself, per entry, before running the batch equation:VerifyingKey::is_weakon the public key, and a directcurve25519-daleksmall-order check on the signature'sR. 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 theRcheck from the public-key one, a real full-order key's own secret scalar used to solve[s]B = R + [k]AforR = identity) — both confirmed to satisfyed25519-dalek's own looseverify(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(newbatch/batch_parallelseries, 4-core host): at 256 entries, one whole-sliceverify_batchcall (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 (newverify_batch_parallel, used wheneververify_new_object_signatureswould 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-coregainssign::verify_batchbehind the new non-defaultbatch-verifyfeature (mkit-clienables it explicitly on itsmkit-coredependency; wasm/Worker builds and other native consumers such ascontrib/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 separatefs::read+decode_ref_wire, one small-file syscall sequence each) sequentially, one at a time — the same "independent per-file work, no fan-out" gapadd's worktree hashing andremote_dispatch's pack/delta/signature fan-outs closed elsewhere, just never closed here (issue tracked from the same family as those). Newrefs::list_refs_with(layout, read_batch)splits the existing behavior into a sequential directory walk (cheap, metadata-only, stays sequential) that collectsRefCandidate { name, path }entries, then hands the whole batch to a caller-suppliedread_batchcallback — the same "collect first, batch-callback second" shapetransfer::plan_pack_with/worktree::store_large_file_streaming_withalready use.read_batchreturns oneRefReadOutcome(Unreadable, dropping the entry, orDecoded(Option<Hash>), keeping it — matching the old inline behavior exactly: an unreadable file was silently skipped, a readable-but-malformed one was kept withRef::hash: None) per candidate, a cardinality mismatch failing closed as a newRefError::RefBatchLengthMismatchrather than silently desyncing names from hashes (mirroringWorktreeError::ChunkBatchLengthMismatch/StoreError::DeltaBatchLengthMismatch's existing contract).list_refs/list_remote_refs/tag listing keep their exact old sequential behavior by default viasequential_read_batch, itself now justcandidates.iter().map(read_ref_candidate)— the newrefs::read_ref_candidateis the single read-and-decode definition shared by that default, mkit-cli's fan-out, and the fan-out bench, so theUnreadable-vs-Decoded(None)policy has exactly one place to drift from. Three new tests pinlist_refs_with's contract directly (sequential-batch equivalence, theUnreadable-vs-Decoded(None)distinction, the length-mismatch error).mkit-cligainscommands::list_refs_parallel/list_tags_parallel/list_remote_refs_parallel(sharing onefanout_read_batchclosure), wired into every CLI call site that previously readrefs::list_refs/list_tags/list_remote_refsdirectly (branch,tag,ref,show-ref,for-each-ref,git's git-export tag/branch export,remote_dispatch's push and pull) — a command likefor-each-reforshow-refthat 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 calledrefs::initbefore writing refs, so everyrefs_update/list_refscase failed outright) is fixed alongside it, since it blocked getting any numbers at all for this change. SemVer:mkit-coregainsrefs::list_refs_with/list_tags_with/list_remote_refs_with,refs::read_ref_candidate,refs::RefCandidate,refs::RefReadOutcome, andRefError::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
advancepays 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 theread_current_descriptorentry 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_chainreplacesadvance's unconditionalfirst_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 newfirst_parent_suffix_to, which walks backward fromtargetonly 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_toreturnsNotFound), 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 unparseablescrubstate 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 fullfirst_parent_chainwalk 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 stalelast_full_verify_unixforces a full walk regardless of cursor position, via a test-only clock override mirroring this file's existingFAIL_AFTERfault-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, includingmany_sequential_publishes_keep_one_generation_until_a_real_rewrite's 40 sequential single-commit publishes — all belowSCRUB_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'ssequential_publishseries (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_dirsequence, 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-coregainsRefError-adjacent internals only —decide_chain,ChainDecision,ScrubState,first_parent_suffix_to,SuffixWalk, and the scrub-file read/write helpers are all private tohistory::ancestry; no public API changes.(core) stop loading the previous publish's whole first-parent chain from disk on every history-mmr publish —
advanceonly ever needed its small, fixed-size descriptor header (repository/full_ref/generation/tip/leaf_count/root), never the up-to-32-MiBchain: Vec<Hash>that follows it in the snapshot file. This follows on fromread_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 comparesold's fields againstchain = first_parent_chain(store, target)— a fresh, full, store-re-verified walkadvancecomputes 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): iftarget == old.tip,chainis byte-for-byteold.chainwithout needing to check — both arefirst_parent_chain(store, old.tip), just computed at different times against the same unchanging objects. The same argument applied to the sub-walk ending atold's length showschain[old.leaf_count - 1] == old.tipis exactly equivalent tochain.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.chainitself was never actually needed.New
read_current_descriptor(replacingread_current_chain/PriorChain) reads only that header, via a newread_prefixhelper bounded toDESCRIPTOR_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_headeris factored out ofdecode_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 guaranteeread_current_chainalready 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/tipcorruption only ever misses a legitimate fast-forward/no-op (fails closed, safe);rootis parsed but never read anywhere inadvance;leaf_countonly indexeschainvia.get, never panics on an out-of-range value;generationis the one field reused directly, but it's independently cross-checked against the separately-readcurrentpointer file beforeadvanceever sees it. No corrupted header field can makeadvancepersist 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 theleaf_count == 1edge case),read_current_descriptor_handles_a_long_branch_name(exercises the bounded-prefix read past the fixed header), andmany_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'shistory_mmr_*/reflog_and_amendintegration 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'ssequential_publishseries 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, andDESCRIPTOR_HEADER_MAX_LENare private tohistory::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
advanceonly needs the previous chain for comparison. Two follow-ups to the double-rebuild fix below:CommitHistorygainedextend, which merkleizes a whole chain in onenew_batch/merkleize/apply_batchcycle instead of one cycle per leaf (each cycle allocates aMerkleizedBatchArc, an overwrites map, and per-height dirty-bucketVecs — real, if modest, per-leaf overhead for something the batch API is explicitly designed to do in bulk; verified againstcommonware-storage's own equivalent "N adds in one batch vs. N single-item batches" tests during review).AncestrySnapshot::build(used by every publish, and bydecode/load) now calls it once instead of loopingappend. Newmem_extend_matches_sequential_append_from_empty/_onto_existing_historytests 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 againststore— purely fewer allocations for the same tree.advance'sread_currentcall loaded the previous publish's fullAncestrySnapshot— decoding its wire bytes and rebuilding its MMR from scratch — purely to readdescriptor/chainfor the no-op/fast-forward/generation-reuse checks;old.mmrwas never touched. A newread_current_chain(backed bydecode_descriptor_and_chain, factored out ofAncestrySnapshot::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 skipsdecode's rebuild-and-compare ofrootagainst 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 becauseadvancenever persistsold's data as-is: every use of it is a comparison againstfirst_parent_chain(store, target), a fresh store-re-verified walk, and only that freshly-verified chain is ever written into the new snapshot;AncestrySnapshot::loadandfinish's own rebuild path are untouched and keep the full check.AncestrySnapshot::load(which does need a working MMR, forprove) 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_lifecyclemkit-cli integration tests) but showed no statistically significant wall-clock change oncargo bench -p mkit-benches --bench history_mmr --features history-mmreven at a 40-sample run (publish/50/publish/250are dominated by this suite's ~10 fsync/sync_dircalls 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-coregainshistory::CommitHistory::extend;append's behavior is unchanged (now implemented in terms ofextend).read_current_chain/decode_descriptor_and_chain/PriorChainare private tohistory::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 thehistory-mmrfeature) used to callfirst_parent_chain(store, target)— onestore.read_object+ deserialize per commit fromtargetall 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 havefinishredo the identical walk and MMR build a second time to actually write it.advancenow keeps the snapshot it already built and hands it tofinish(finishtakes a newprebuilt: Option<AncestrySnapshot>, trusted only when its repository/full_ref/generation/tip exactly match the pendingTransaction; crash recovery viarecover, which only ever has aTransactionread back from disk, keeps passingNoneand rebuilding as before).cargo bench -p mkit-benches --bench history_mmr --features history-mmr(10-sample criterion A/B, this branch's host):publish/5018.8ms → 7.5ms (−60%, p=0.01),publish/25020.9ms → 12.5ms (−40%, p<0.01). SemVer: none —finishis private tohistory::ancestry; no public API change.A second, more aggressive change was prototyped alongside this — a
first_parent_chain_fromfast 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-walkingtarget's entire ancestry fromstoreevery time (themkit commitsteady state would have gone from O(history depth) to O(new commits) per publish; a newhistory_mmr/sequential_publishbench added to measure it is kept). It was reverted before landing: a correctness review found it stopped re-verifying the reused prefix againststoreon every publish, silently narrowing the "validate the target from verified objects before persisting intent" guaranteeadvance'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 callsAncestrySnapshot::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 aboveworktree::CHUNK_THRESHOLD—mkit add, andstatus/diffon any stat-mismatched large file) copied every byte twice per chunk:fillread into a separate 64 KiB scratch buffer andextend_from_sliced it into a growing window, thennext_chunkcut the window withVec::split_off(a fresh allocation plus a memcpy of the remainder) andmem::replace. It now reads into a single fixed4 * max_size(1 MiB) buffer withstart/endcursors advanced in place, compacting withcopy_withinonly once the tail runs out of room (amortized over severalmax_sizewindows, not every chunk); a newnext_chunk_refreturns a borrowed&[u8]for callers that don't need to own the bytes, andworktree::store_large_file_streaminguses it to avoid a further per-chunk copy. Chunk boundaries are unchanged (sameFastCdc::cutover the same bytes) — the existingstreaming_matches_in_memory_*/proptest_streaming_matches_in_memorytests all ran on inputs under the 1 MiB window and so never actually exercisedfill'scopy_withincompaction path; a newstreaming_matches_in_memory_iterator_across_multiple_compactionstest (6 MiB, forcing several compactions) closes that gap, checking bothnext_chunkandnext_chunk_refoutput againstChunkIterator. Newcargo bench -p mkit-benches --bench chunker_streamingsuite (in-memoryCursorsource, 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::chunkergainsChunkReader::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_packused tostore.read+delta::encode(disk read + block-hash-table build + greedy scan, all sequential) once per delta candidate while planning a push;transfer::plan_packnow delegates that loop to a newtransfer::plan_pack_with(..., encode_deltas)batch callback, andmkit-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, isolatingdelta::encodeover 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-coregains 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-suppliedhash_chunkscallback instead of hashing chunks one at a time;hash_file_with_metadata_withexposes that callback for file ingest;hash_file_with_metadataretains the borrowed sequential path.mkit-cli'saddpath (commands::add::hash_pending) passes a rayon fan-out once a batch is large enough to amortize dispatch cost (4 chunks/thread, mirroring the existinghash_fanout_thresholdshape) — but only whenadd'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, sohash_pending_batch'spar_iterbranch now hashes each file's chunks sequentially instead. The originalcargo bench -p mkit-benches --bench chunk_hash_fanoutmeasurements (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_metadatavs_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-coregains 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 as2 * 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. Newtransfer::encode_delta_candidate_with_base(mkit-core) is the per-candidate unit that takes already-read base bytes instead of readingbaseitself;transfer::encode_delta_candidateis unchanged for the sequential default's benefit. SemVer: additive — new publictransfer::encode_delta_candidate_with_base; existing behavior unchanged. (core) stop decoding a
Tree/ChunkedBlobtwice on everyObjectStore::read_objectcall.read_objectused to callSelf::read(which, for a merkelized type, alreadydeserializes the bytes once to recompute the BMT-root id for the integrity check) and then callserialize::deserializea second time on the same bytes to produce theObjectit returns — two full tree/chunked-blob parses for every read, on one of the hottest paths in the codebase (read_objectis the primitive every tree-walker,diff,blame,merge,rebase,restore,stash,bisect, andgraphop reads commits and trees through). Newobject::verified_id_and_objectdecodes once and hands back both the verified id and the decodedObject(Nonefor a byte-hashed type, or when the merkle-decode itself failed — the same fallback-to-hash(bytes)object_id_from_bytesalready had, now shared by both);read_objectreuses the decoded object directly instead of decoding again, andobject_id_from_bytesis now a one-line wrapper around the same function so the two callers can never drift. Alongside it,serialize::read_treestopped cloning every entry's name into a separateprev: Option<Vec<u8>>purely to compare it against the next entry's name for the ordering check — the just-pushedTreeEntryalready owns that name, soentries.last()gives the same comparison for free.Caught in review: the inherent-method fix alone missed the actual hot path.
store::ObjectSource(the traitdiff::load_tree,worktree::blob::LoadedBlob::load, and every otherS: ObjectSource + ?Sized-generic caller actually call through) has its own defaultread_object(deserialize(self.read(h)?)) — the exact same double-decode pattern — andimpl ObjectSource for ObjectStoreonly overroderead/read_unverified, notread_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 alwaysObjectStore.impl ObjectSource for ObjectStorenow also overridesread_objectto 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 privatecheck_hashhelper. New tests pin both fixes directly:read_object_deserialises_tree/read_object_detects_tree_corruption_via_store_and_traitcallread_objectthrough&dyn ObjectSourcespecifically (the same static-dispatch shapeload_tree/LoadedBlob::loaduse) and assert it matches the inherent method on both the happy path and a corrupted-treeHashMismatch.A second, broader review round (8 parallel angles against the full
origin/main...HEADdiff) found one more sibling gap, independently flagged twice:EphemeralSink(the in-memory overlaydiff/statusread staged trees through) implementsObjectSourcetoo but never overroderead_objecteither, 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 EphemeralSinknow overridesread_objectthe same way: an overlay hit decodes the already-trusted bytes once directly, a miss delegates toself.store.read_object, reusing the now-fixed store-side decode. Newephemeral_sink_read_object_tree_overlay_and_fallthroughtest 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_readsuite (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 throughObjectSource, on bothObjectStoreandEphemeralSink. All 790 mkit-core tests pass, includingread_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_objectandcheck_hashare private;ObjectStore's,EphemeralSink's, andObjectSource's public signatures are unchanged, and bothimpl ObjectSourceblocks gaining aread_objectoverride 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-outaddalready had.ops::restore::restore_blob'sChunkedBlobarm read every chunk (open + BLAKE3-verify + decode, viaObjectStore::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" gapchunk_hash_fanout's own doc already names for the write direction. Newops::restore::restore_tree_to_worktree_withtakes an explicitread_chunksbatch callback (mirroringworktree::store_large_file_streaming_with'shash_chunksshape, for the same reason:mkit-corehas 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 ofRESTORE_CHUNK_BATCH(64, matchingworktree::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 typedRestoreError::ChunkBatchLengthMismatchrather than a silent desync.restore_tree_to_worktree(unchanged signature) is now a thin wrapper passing a sequential default.mkit-cli's newrestore_fanout::read_chunks_fanoutwires a rayon fan-out into every checkout/clone/reset/restore/sparse-checkout call site (commands::{checkout, clone, reset, restore, sparse_checkout}andcommands::restore_worktree_and_index), using the same "sequential below a threshold,par_iterat or above it" crossover shape ascommands::add's own chunk-hashing fan-out (crate::fanout::try_map_seq_or_par).New
cargo bench -p mkit-benches --bench restore_chunk_fanoutsuite (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, sequentialrestore_tree_to_worktreevs fully rayon-fannedrestore_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 sequentialwrite_allinto 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 existingops::restoreunit tests (includingrestore_chunked_blob_reassembled/restore_rejects_chunked_total_size_mismatch) andmkit-cli's checkout/clone/reset/restore/sparse-checkout integration tests pass unchanged;cargo clippy --all-targets --all-features -- -D warningsandcargo fmt --checkare clean. SemVer: additive —mkit-coregainsops::restore::restore_tree_to_worktree_withandRestoreError::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 tocommonware_storage::bmt::Proofat the pinned2026.9.0train, replacing the prior provisional format.Proofis a new public struct (leaf_count: u32,siblings: Vec<Hash>) encoded asu32 BE leaf_count ‖ varint(n) ‖ n × 32-byte digestviacommonware-codec, decoded throughProof::decode(bytes, max_items)with allocation bounded tomax_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_proofare replaced bybuild_{chunk,tree_entry}_proofand id-based verifiers —verify_tree_entry,verify_chunk, and their range/multi counterparts — which check against the object's id (via the newwrap_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_chunkand its range/multi counterparts additionally reject position 0 (theChunkedBlobmetadata leaf) outright. A new native-only cross-check test (merkle::tests::proofs_match_commonware) proves byte-identity and mutual verifier acceptance againstcommonware_storage::bmtacross randomised trees and single/range/multi positions; golden vectors live underrust/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_idand 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 toO(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 formkit commit's one-append-per-publish pattern.CommitHistory(mkit-core::history) now wrapscommonware_storage::merkle::mmb::mem::Mmbinstead ofmerkle::mmr::mem::Mmr; both are the same genericMem<Family, Digest>machinery (Clone, batch API, pruning, proof verification) with only theFamily(peak/position topology) differing, so the swap was a mechanical type-and-terminology change confined tohistory.rs—ancestry.rsand everywhere else in the codebase only ever used mkit's ownPosition/InclusionProof/CommitHistorywrappers, never MMR types directly.HistoryError::Mmris renamedHistoryError::Mmb; thehistory-mmrCargo 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
InclusionProofbytes 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, thedigestspeak-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 keepsBagging::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'shistory_mmr_*/reflog_and_amendintegration 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'spublish/loadseries (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-onlybench_in_memory_build(batchedextendover 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-appendlatency 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 againstcommonware-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 orInclusionProofbytes under the prior MMR-based format, or matched onHistoryError::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
.witnessfiles; 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, rayonpar_iterat 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 sharedfanout::thresholdformula. Newfanout::map_seq_or_par/fanout::try_map_seq_or_parfactor that shape out for the four call sites it fits exactly (by-reference, in-order, infallible orResult-collecting);prepare_delta_batch(consumes by value) andverify_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.1release train to2026.9.0across 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 takeselfby value;CommitHistory's journaled backend handles a failed mutation by leaving the handle poisoned (HistoryError::Poisoned) instead of panicking, androot()/len()fall back to the last known-good value rather than requiring aResult.commonware-runtime2026.9.0 added a per-storage-directory advisory.holdfile lock, which deadlocked opening a second branch's history journal in the same process;CommitHistorynow shares one bootstrappedContextper<mkit_dir>/historydirectory across every branch opened in-process (seedocs/INVARIANTS.md, "One commonware storage Context per history dir per process").threshold::recoverdropped itsFaultsgeneric andsharing::Modelost itsDefaultimpl;mkit-attest's BLS threshold signer namesMode::NonZeroCounterexplicitly (same value the old default resolved to — pinned by a new golden-vector test).commonware_parallel::Strategycan no longer be implemented outside thecommonware-parallelcrate (Manual::newwas removed);pack_shard.rs's spy-basedCountingStrategytest was removed accordingly (seedocs/INVARIANTS.md).- blst is now opt-in upstream (only pulled in via
commonware-cryptography'sstd→bls12381feature 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-encis deprecated: there is no maintained server for it. Use SSH (mkit serve) or Connect.
Removed
mkit serve --httpandmkit serve --listen-enc.mkit serve <PATH>is now only themkit+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 orSQLite. 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, andmkit-cli'shttp-transportcargo feature (enc-transportstays: it is the deprecatedmkit+enc://client). The pre-production policy allows the removal without a deprecation period.- Standalone server binary and container image. There is no
mkit-serverbinary, release archive orghcr.iocontainer image, and no native SQLite/S3/filesystem server deployment. Serve repositories over SSH withmkit serve, or on Cloudflare Workers with themkit-server-workeradapter. Themkit-serverlibrary crate and the Workers adapter remain. mkit-transport-connect'sservercargo feature (breaking). The axum-hosted server that backedmkit serve --httpis gone, with its public API:serve,router,TransportServerandmap_transport_error, and theaxumandconnectrpc-healthdependencies. The crate is now themkit+https://client (ConnectTransport) only; servemkit.transport.v1with themkit-serverlibrary or its Workers adapter. Removing a published feature and public items is semver-breaking, so the next release ofmkit-transport-connect(and, with lockstep versioning, everymkit-*crate) is 0.5.0, done at the release that merges themkit-serverwork tomain(cargo-semver-checks runs at the tag,crates-publish.yml). crates.io listsmkit-clias its only published dependent, and no publishedmkit-clienables the feature by default.mkit-server-conformance's legacymkit serve --httpwire 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 callslibc::sync(), which does not exist onx86_64-pc-windows-msvc— the workspace and its test suite (not just the default-featuremkitbinary) 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: thewindows-smokeCI job, thex86_64-pc-windows-msvcrelease leg (and its.ziparchive), theinstall.ps1installer, the Scoop packaging manifest andrelease-verify.yml's Scoop channel check, and thewindows-credentialleg of thekeystore-backendsCI matrix are all gone.mkit-keystore'sbackend-windows-credentialfeature and itsBackendKind::WindowsCredentialManagervariant were removed —"windows-credential"is now an unrecognized backend name (key backend: .../ exitCONFIG_ERROR) rather than a compiled-in-but-platform-unavailable one (previously exitUNAVAILABLE, "requires Windows"). Removing a public enum variant from a published crate is semver-breaking:mkit-keystore(and, per the workspace's lockstep versioning, everymkit-*crate) needs a0.5.0release, not0.4.3, to ship this. Windows users should run mkit under WSL, which uses the Linux binary — seedocs/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: apayload_lennearu32::MAXmadepos + payload_lentrap on wasm32 (release builds keepoverflow-checks) inPackEntries::new,delta_base_hashesand, through them,verify_closure_packs(exported by mkit-wasm). It is nowPackError::UnexpectedEof; a wasm32 test inmkit-core-wasm-checkpins 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
ChunkedBlobrange whoseoffset + lenoverflowsu64(e.g.mkit prove --range 10:18446744073709551615) or on a chunk shorter than its 10-byteBlobprologue: these now returnVerifyError::OffsetOverflowandVerifyError::Decode(UnexpectedEof). Valid bundles are byte-identical. - (core) A
Rangepayload over a chunked leaf (chunk = Some(hdr)) now rejectslen == 0before 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 negativesneg_zero_length_range_chunkedandneg_range_chunk_meta_forged_total_size. SemVer: none. - (core)
verify::resolve_absolute_offset's finalsum + offset_in_blobaddition now useschecked_add, matching every neighbouring offset computation, and returnsVerifyError::OffsetOverflowon 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 atMAX_BUNDLE_BYTESinstead of buffering an unbounded stream before the size check ran, rejecting an oversized bundle withDATAERRinstead of blocking or exhausting memory on an adversarial or unbounded input. SemVer: none. - (cli)
mkit_verify_proof's MCP tool descriptor no longer claimsreadOnlyHint = true— itspayload_outfield writes a file, so it now matchesmkit_prove's hints (not read-only, not destructive, idempotent). SemVer: none (annotation-only). - (workspace-worker)
authenticatenow 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 intermittentworkspace.integration.test.tsfailure. SemVer: none — worker-only, no public API. - (core)
verify_closure/verify_closure_packsmerge the walker'scorruptlist 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-unknownnow compiles: on wasm32,getrandom0.4 is enabled with thewasm_jsfeature (same posture asmkit-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 catchesStoreError::HashMismatchper object, reports it under theClosureReport'scorruptlist (removing it frommissingfirst, if the walk had already reached it), and continues, matching the command's documented fsck-shaped behavior. Also hides thenote: N unreferencedlist (and the JSONunreferencedarray) in local mode unless a new--show-unreferencedflag 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 expectsbad: closure incomplete: 0 missing, 1 corrupt. SemVer: additive (new flag; local-modecorrupt/unreferencedreporting changes for a case that previously hard-failed the whole command). - (core)
verify::resolve_absolute_offsetnow rejects a non-emptychunk_len_proofson aRangepayload 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-Blobleaf (VerifyError::UnexpectedLengthProofs, now shared by both cases). New unit testlen_proofs_on_chunk0_are_rejectedand golden negative vectorrust/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 advisorywrite_scrub_statecall (history-mmr) no longer fails the whole publish if the write itself fails —finishhas 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 theScrubStatepanic 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, whichfirst_parent_chain(store, target) == first_parent_chain(store, d.tip) ++ suffixmakes 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 parallellist_remote_refs_parallelfan-out every other ref-listing call site already got when that fan-out was added — this one spot was missed. - (core)
HistoryErroris now#[non_exhaustive], matchingGcRootsError's existing convention in the same crate — whichcommonware-storagestructure backs the ancestry proof (theMmbvariant'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)
RefErroris now#[non_exhaustive]too, for the same reason and found in the same code-review pass — it gainedRefBatchLengthMismatchin 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 freshChainDecision::NewGeneration— full walk plus a freshScrubState— via separately hand-copied code; factored into onenew_generationhelper both branches call, found as duplication by code review. No behavior change. - (core)
decode_descriptor_and_chain's minimum-payload-length check andDESCRIPTOR_HEADER_MAX_LENeach independently hard-coded the 143-byte fixed descriptor header size (as175—143 + 32for the trailing checksum — and143, respectively), found as duplication by code review. Both now derive from one newDESCRIPTOR_HEADER_LENconstant. No behavior change. - (core)
read_prefix(history-mmr's bounded prefix read, used byread_current_descriptor) now preallocates its buffer to the caller's knownmax_bytesbound instead of lettingread_to_endgrow it via generic doubling — found as a minor inefficiency by code review;max_bytesis small in every call this crate makes (currentlyDESCRIPTOR_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 singleBlobcan be up toworktree::CHUNK_THRESHOLD(1 MiB — mkit chunks anything larger), so averify_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 andtransfer::plan_pack_with'shash_chunks/encode_deltasbatch-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_withhad no check at all) or panicking (plan_pack_with's priorassert_eq!) — a buggy or third-party batch callback now fails the operation cleanly rather than either corrupting aChunkedBlobmanifest or crashing the process mid-push. Chunk hashing contract failures returnexit::SOFTWARE(70, sysexitsEX_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(viaremote_dispatch's per-branch fast-forward and unpack/publish locks) andmkit status's opportunistic stat-cache refresh took the worktree lock directly instead of throughcommands::acquire_worktree_lock, silently skipping the "this root is being served bymkit serve" warning thatcommit/checkout/gcalready print in the same situation (SPEC-CONCURRENCY §3.1).commands::warn_if_servedis now called at all four sites; newserve_guard.rsregression 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 rangepanic) inhistory-mmrancestry 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 advisorywrite_scrub_statecall runs strictly afterfinishhas 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 oldverified_throughand slice a chain far too short for it.ScrubStatenow 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'sslice index out of rangepanic (above), found by a follow-up code review of that same fix: thegenerationbinding rules out a stale scrub file (computed against a different, superseded generation), but not one whoseverified_throughsimply 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 newerscrubfile.decide_chainnow checks the scrub window's end bound against the real prefix length before callingverify_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_windowitself now also bounds-checks viaslice::getand returns a typedHistoryError::Corruptedrather 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 argumentdecide_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 advisoryscrubfile — 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", andwrite_scrub_state's caller already treats a write failure as advisory-only (let _ = write_scrub_state(...)), butread_scrub_statestill used?onread_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 thescrubpath throughout and confirms the second publish — the first fast-forward that actually reachesread_scrub_state— no longer fails; the existing write-side test only ever exercises a first publish, whereread_scrub_stateisn't reached at all. SemVer: none. - (cli)
mkit mcp --http <addr>now refuses to bind without authentication, matchingmkit serve --http's fail-closed design. Previously it bound the given address (not restricted to loopback despite its own doc comment's claim) with noAuthorizationcheck at all — any network-reachable caller got unauthenticated access to the full MCP tool catalog, including mutating tools likemkit_checkout. It now requires a bearer token (--http-token <TOKEN>or theMKIT_MCP_TOKENenv var — a name of its own, notserve --http'sMKIT_API_TOKEN, since the two surfaces have different threat models and must not share a secret) or an explicit--unsafe-allow-any-http-peeropt-out that prints a loud warning, enforced on every request via a newBearerAuthHttptower middleware wrapped aroundStreamableHttpService. Newmcp_v2_http.rsmod authintegration 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/wrongAuthorizationheader, success with the right token, and theMKIT_MCP_TOKENenv fallback. SemVer: additive — new CLI flags, new env var; existing--httpusage 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-rpcschemas (common.proto/signer.proto/ssh.proto/verify.proto) andapps/repo-worker'srepo.proto(RepoService) are now named modules in the repo-rootbuf.yamlv2 workspace, pushed to the Buf Schema Registry asbuf.build/officialunofficial/mkit-rpcandbuf.build/officialunofficial/mkit-repoon every tagged release (newbuf-pushjob incrates-publish.yml, dormant untilBUF_TOKEN/BUF_PUBLISH_ENABLEDare provisioned — seedocs/RELEASE.md). Third-party signer integrators (HSM/TPM vendors, custodial signing services) andRepoServiceclients can nowbuf generatetyped bindings from a pinned tag instead of vendoring this repo — see the checked-inbuf.gen.yamlreference recipes in each module's proto directory.contrib/signers/mkit-sign-se's checked-in Swift bindings are now regenerated viascripts/regen-mkit-sign-se-swift.sh(buf generate, not rawprotoc) and refreshed to include the previously-missingALGORITHM_BLS12381_THRESHOLDcase. mkit serve --http <addr>: self-hosted Connect remote (SPEC-TRANSPORT-CONNECT).mkit servecan now hostmkit.transport.v1.TransportServiceover axum/HTTP instead of the SSH-frame protocol, behind the newhttp-transportcargo feature — an operator without SSH access or a cloud object store can run a real, testablemkit+https://remote against a local repository (server-side half of themkit-transport-connectcrate, behind that crate's ownservercargo feature; generic over anymkit_core::protocol::Transportbackend; instantiated today overFileTransport). All seven wire RPCs (ListRefs/ReadRef/UpdateRef/AdvanceRefs/PackExists/UploadPack/DownloadPack) run the underlyingTransportcall on a blocking task so a synchronous CAS ref write never stalls the async executor.UploadPack/DownloadPackvalidate 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-peerdevelopment escape is supplied;Ctrl-C/SIGTERMdrain in-flight requests before exiting. Themkit.transport.v1proto (proto/mkit/transport/v1/transport.proto) is generated viabuffa/connectrpc-build, vendored undergenerated/the same waymkit-repo-client/apps/repo-workeralready do — seedocs/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 byteswhile building/uploading the outgoing pack,Unpacking objects: N objectswhile applying a downloaded one — using only real counts (objects actually staged/unpacked, bytes actually handed to the transport). mkit still never fabricates git'sEnumerating/Counting/Compressing objectsorTotal N (delta D)lines, perdocs/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/--quietflag on all four commands forces it off, andMKIT_PROGRESS=always/neveroverrides the tty auto-detection explicitly (mirrorsNO_COLOR/CLICOLOR_FORCE). - Native ConnectRPC client for
mkit+https://(SPEC-TRANSPORT-CONNECT, #701). Themkit-transport-connectcrate's mandatory baseline (always compiled, independent of itsserverfeature above) is now a non-wasm ConnectRPC client formkit.transport.v1.TransportService, generated fromproto/mkit/transport/v1/transport.protovia the same vendored-codegen patternmkit-repo-client/apps/repo-workeruse (noprotocneeded on the default build path;MKIT_REPO_CODEGEN=1plusscripts/regen-transport-proto.shto regenerate).mkit-cli'sremote_dispatchnow constructs this transport formkit+https:/// loopbackmkit+http://, replacingmkit-transport-http's bespoke JSON dialect there. TLS trust is pure-Rust (webpki-roots, no OS trust store); the synchronousTransporttrait is bridged to the async generated client via a dedicated per-instance tokio runtime, mirroringmkit-transport-enc'sTokioExecutor.ConnectTransport:: supports_atomic_advance()defaults tofalse(opt in viawith_atomic_advance(true)) until a confirmed-transactional reference deployment exists.mkit-transport-httpis NOT removed: itssparse-checkout/pack-shardsextensions have nomkit.transport.v1equivalent 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-sizePackChunk { offset, data, last }segments instead of oneVec<u8>, reusing the same chunk shapemkit-rpc'sssh.protoalready defines. Both have default implementations expressed in terms of the existing whole-bufferupload_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-sshandmkit-transport-encoverride both to forward chunks straight to thePackChunkframe 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-httpkeeps the default buffer-then-delegate behavior for now — real HTTP pack streaming arrives via themkit.transport.v1Connect service's client-/server-streamingUploadPack/DownloadPackRPCs (SPEC-TRANSPORT-CONNECT §6, pending #698/#701) — but its per-retry full-body clone inupload_packis fixed separately: the request body is nowBytes(refcounted) instead ofVec<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-parentanddiff -w/-b/-U<n>(#712).mkit logfilters commits by a substring match on the author identity (--author) or commit message (--grep), by a--since/--untiltimestamp bound (accepting@<unix-seconds>,now/today/yesterday,<N> <unit> ago, orYYYY-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 diffgains-w/--ignore-all-spaceand-b/--ignore-space-change(whitespace-insensitive line comparison;-wwins if both are given) and-U<n>/--unified=<n>(context-line count, default 3). Both--author/--grepare plain substring matches rather than regexes (mkit identities are opaque, not free-text names) and--since/--untiluse a small explicit date grammar rather than git'sapproxidate— 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,.ziparchive instead of.tar.gz,mkit.exe— built with thebackend-windows-credentialkeystore feature (already the default on Windows viamkit-cli's[target.'cfg(windows)'.dependencies]stanza), matching parity with the already-testedwindows-credentialleg ofrust.yml's manual-onlykeystore-backendsmatrix. The Windows archive goes through the same cosign keyless signing and mkit-native DSSE release attestation as every other target. Newinstall.ps1at the repo root is the native-Windows counterpart toinstall.sh(same trust model: cosign-required by default, downgrade guard, atomic install) — served athttps://mkit.sh/install.ps1alongside the existinginstall.sh. Newcontrib/scoop/mkit.jsonmanifest template (mirrorscontrib/homebrew/mkit.rb) un-defers the Scoop-manifest checklist item indocs/RELEASE.md.install.shitself still targets Darwin/Linux only — it now points MINGW/MSYS/Cygwin users atinstall.ps1instead of failing with an unsupported-OS error. Known gap:mkit self updatedoes 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 viainstall.ps1or a future Scoop bucket instead ofmkit self updatefor now (#714, part of #676). - SLSA build provenance for release archives. The
releasejob in.github/workflows/release.ymlnow generates a standard SLSA build provenance attestation (actions/attest-build-provenance, GitHub/ Sigstore-native) over everydist/*.tar.gzanddist/*.ziparchive (including the Windows leg), staged alongside the existing cosign signatures and the mkit-native DSSE attestation asmkit-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. Seedocs/RELEASE.md("Verify the SLSA build provenance attestation"). - Packfile v2: per-entry zstd compression (SPEC-PACKFILE §3.3, §3.4).
PackWriter/PackReadertransparently compress/decompress pack entries — two new entry types,0x03zstd-raw and0x04zstd-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_deltacompress 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 emitsversion = 1when a pack has no compressed entries andversion = 2the moment it has at least one;0x03/0x04are illegal inside aversion = 1pack and rejected asInvalidEntryTypeif seen there. Old (pre-v2) readers hitting a v2 pack fail closed withUnsupportedVersion— 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 (newPackError::DecompressedSizeOverCap/DecompressedSizeMismatch/ZstdEntryTruncated/ZstdDecompressvariants). 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); seerust/benches/benches/pack_compression.rs. New directzstddependency onmkit-core(behind apack-zstdfeature, on by default) — already present transitively via thecommonware-storagedependency stack at the same resolved version (0.13.3), so this adds no new supply-chain root.mkit-wasmandapps/repo-worker(both compile to wasm32-unknown-unknown) opt out (default-features = false) sincezstd-syscannot target that platform. mkit-core: CAS-guarded ref delete. New publicrefsprimitivesdelete_ref_if_matchesand (on--features history-mmr)delete_ref_with_history_if_matches— adelete_ref/delete_ref_with_historythat only removes the ref (and, on history-mmr builds, its journal) when its current on-disk value is exactly the caller-suppliedexpectedhash, using the same per-refcas_lock_namelockupdate_ref'sMatcharm already takes.mkit branch -mnow 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,boundaryon a file-history root, andfilename) — 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=jsonand thelogprecedent: 64-hex ids;author/committercarry mkit's Identity (empty*-mail,+0000tz, single UTC author = committer);filenameis the-Ccopy source on a cross-file copy; git'spreviousline 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–127else=bad,≥128or signal=abort), converging on the first bad commit. The candidate is also exported asMKIT_BISECT_COMMIT. mkit's bisect stays print-candidate by design, sorunchecks 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,runreports 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 (gitcheckout -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 bybisect 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-tagreceipt written byinstall.shsits 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 — nocosign, no GitHub attestation API — plus the sha256 sidecar as defense-in-depth, and the staged binary must pass aversionself-check before an atomic same-directory swap. Downgrade policy mirrors the installer (latestnever downgrades; explicit--versionpins need--allow-downgrade), receipts are rewritten in the installer's exact format, and--check/--format jsonreport 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 atdocs/keys/release-attest.pub; custody plus rotation runbook indocs/RELEASE.md).release.ymlself-verifies the envelope against the checked-in public key before publishing, and the envelope is covered by the cosign-signedSHA256SUMS. Produced by the new internalmkit-release-attesttool crate (publish = false). mkit blameline ranges and revision argument.blamenow accepts-L/--linesto 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 onlyHEAD. Range semantics and diagnostics matchgit 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 (likegit 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/-Cmove 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-Cwidens 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 typedMoveDetection/CopyDetectionAPI that can't express an invalid "enabled but zero-threshold" state. Detection is merge-aware (#499): at a merge both-Mmoves and-C -Ccopies 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 — matchinggit 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'sblame.c, each shape pinned by a test with its git recipe): the-M/-Cpass 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-Msource, and its-Ccandidates 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-Msource 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-Clevel 1 too; a parent that deleted the blamed file can still supply the-C -Ccopy 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 -Cwhole-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 -Cwas approximated as-C -Cand missed it. Pinned against git 2.50.1.mkit blame -Ccopy 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, likegit 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, matchinggit blame --ignore-revat a merge.--ignore-revis repeatable and accepts any revision (short hash, ref,HEAD~2);--ignore-revs-filereads full hex object names one per line, skipping blank lines and#comments (including inline) — both verified against realgit. 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 blanket128. mkit does not auto-read.git-blame-ignore-revsor ablame.ignoreRevsFileconfig key — pass the file explicitly.mkit blame --reverse <start>..<end>. Walks history forward instead of backward, likegit 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>toHEAD; an explicit<start>is required, and-w/-Lcompose with it. Verified field-by-field against realgit 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 bydocs/specs/SPEC-MERKLE-OBJECTS.mdand the code they annotate),docs/research/(2 exploratory design notes),docs/keystore.md(a redundant stub — seedocs/specs/SPEC-KEYSTORE.md), anddocs/INVARIANTS.md(unreferenced anywhere in the repo, scoped to the now-closed #634 perf epic). - Removed the mkit-native release-attestation feature.
mkit self updateno longer requires, or is able to verify, the DSSE/in-toto envelope previously signed over release tarballs by therelease-attestcrate; verification is now sha256-sidecar-only, and a release that omits the sidecar installs with no cryptographic check at all beyond TLS.cosignsignatures and the GitHub-native SLSA build-provenance attestation on every release are unaffected and remain the recommended verification path forinstall.sh. Breaking (SemVer, pre-1.0 minor bump): removes the publicmkit_cli::commands::self_update::verify_release_attestationfunction and theUpdateEnv::trust_keysfield. Also removesrust/crates/release-attest,docs/keys/, and the correspondingrelease.ymlsign/verify steps; theMKIT_RELEASE_ATTEST_KEYGitHub 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 completeddocs/MERKELIZATION-PLAN.md(the work shipped — seedocs/specs/SPEC-MERKLE-OBJECTS.mdand ADR 0001). The license grant (LICENSE-MIT,LICENSE-APACHE,NOTICE),SECURITY.md,CODE_OF_CONDUCT.md,CONTRIBUTING.md, and thegeigerunsafe-code ceiling check are retained.
Changed
- commonware 2026.7.0 dependency train across the workspace (up from
2026.5.0), plus
apps/repo-workerand the separatecontrib/signersworkspace, both of which path-depend intorust/crates/*and needed their ownCargo.lockregenerated too. No wire-format or object change — investigated droppingmkit-core/src/merkle.rs's vendored BMT construction for a directcommonware_storage::bmtdependency now thatbmtis confirmedno_std(commonwarexyz/monorepo#4090), but found it's not viable:bmt::Builder<H: Hasher>requirescommonware_cryptography::Hasher, andcommonware-cryptography'sblst(BLS12-381) dependency is unconditional, not feature-gated, and fails to build forwasm32-unknown-unknownon toolchains without a WASM-capable C compiler. The vendored construction stays (its cross-check test against upstream still passes byte-for-byte).commonware-parallel'sStrategytrait gained new required methods (manual,spawn,run,try_run,try_fold,sort_by); mkit's internalCountingStrategytest helper was updated to match.commonware_utils::test_rng_seeded(seed)was renamed toTestRng::new(seed)andcommonware_storage::freezer::Freezer::initgained a thirdOption<Checkpoint>parameter; both updated at their mkit call sites. - BREAKING (
mkit-attest,bls-thresholdfeature):trusted_dealer/bls_threshold_trusted_dealer's generic RNG bound moved from rand_core 0.6'sCryptoRngCoreto rand_core 0.10'sCryptoRng— a from-scratch, source-incompatible trait (TryRng/TryCryptoRng), not just a version bump.commonware-cryptography2026.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'sCryptoRng(for examplecommonware_utils::TestRng, orrand_core::UnwrapErr(getrandom::SysRng)in place of the oldrand_core::OsRng); an RNG that only implements the 0.6-eraCryptoRngCoreno longer satisfies the bound. - BREAKING (
mkit-core,pack-shardsfeature): pack-shard Reed-Solomon hasher cut over fromSha256toBlake3.pack_shard::RsSchemenow wrapscommonware_coding::ReedSolomon<Blake3>instead ofSha256, 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 BLAKE3shard_hashesenvelope check, which is unchanged).MANIFEST_VERSIONis bumped0x01→0x02since the wire-visibleShardSet::commitmentvalue changes; this is a hard cutover, not dual-hasher support — a0x01manifest 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, and0x01is retired permanently. Producers and consumers on differentpack-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 "Sequentialparallel 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, theworktrees/registry, discovery (and its fail-closed matrix), the lock model, and cross-tree gc semantics.git worktreeleaves PARITY.md's v1 non-goals list with a scope amendment recording the deliberate divergences (per-tree stash;move/lock/repairremain follow-ups); SKILL.md, the man page, and the bash/zsh/fish completions gain theworktreecommand. - Cross-worktree gc plus the lock split (#493 Phase 3).
mkit gcis 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 — untilworktree prunereaps 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 ofstatus/reset --hard/clean/rm/stashacross 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(--porcelainfor scripts) shows every tree with its HEAD,removedeletes a tree but refuses to destroy local changes, untracked files, or an in-progress operation without--force, andprunereaps registry entries whose tree vanished (--dry-runpreviews). A branch may be checked out in at most one tree:worktree add,checkout/switch,branch -d, andbranch -mall refuse withalready checked out at '<path>'(branch moves are single-writer through the history-MMR ref path). Registry mutations serialize on a new common-dirworktrees.lock;addorders 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::layoutgains the linked-worktree groundwork: a linked tree's.mkitis a pointer FILE (mkitdir: <path>, the analog of git'sgitdir:file) naming its per-tree state dir under the main repository's.mkit/worktrees/<id>/(withcommondirand amkitdirback-pointer inside, git-style);layout::discoverresolves it — a.mkitdirectory or absent.mkitstill yields the classic single-worktree layout byte-identically, while a malformed, oversized, or dangling pointer fails closed with a typedDiscoverError. The CLI'scommands::resolve_layoutnow 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-facingworktreecommand yet (that lands withmkit worktree add/list/remove/prune); linked trees can only be assembled by hand at this stage. RepoLayoutpath-resolution seam (#493 Phase 0).mkit-coregainslayout::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&RepoLayoutinstead of a bare&Pathroot:ObjectStore::{open,init}, all ofrefs,index::{read_index,write_index,index_path},ops::{conflict_state, rebase, bisect, stash, gc, recovery},ops::restore::{load,write}_ sparse_checkout,CommitHistory::open_at(andCommitHistory::common_dir()replacesCommitHistory::mkit_dir()),mkit_attest::store, andmkit_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 throughcommands::resolve_layout, the future discovery seam.- buffa 0.8.1 plus connectrpc 0.8.0 everywhere. The main workspace and
contrib/signersmove buffa 0.8.0 → 0.8.1 (runtime-only patch; vendored codegen verified byte-identical). The ConnectRPC pair (mkit-repo-clientplusapps/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'simpl_default_instance!runtime macros, fusedput_*_fieldwriters, and sharedcheck_wire_type/map_codechelpers (wire output is byte-identical). Handler traits now returnimpl Encodable<Resp>, letting future handlers return zero-copy views. connectrpc's now-optionaljsonfeature is enabled explicitly so generated serde derives and JSON error/end-of-stream frames keep working on wasm. mkit blameis 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 — matchinggit blame's default. This changes output for histories with merges; the new--first-parentflag (likegit blame --first-parent) restores the previous first-parent-only attribution and composes with-w/-M/-C/--ignore-rev. Verified field-by-field against realgit(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, not128.)- BREAKING (
mkit-core):TreeandChunkedBlobare now content-addressed by a domain-bound Binary Merkle Tree (BMT) root (id = domain_digest(TYPE_DOMAIN, bmt_root)) instead ofBLAKE3of their serialized bytes; every other object type keeps the flat scheme. Because aTreeid feeds itsCommitid 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 andschema_version = 0x01are unchanged — the break is in object identity only. Cross-format safety is a mandatory.mkit/formatrepo marker (bmt-v1): a pre-merkle repository is rejected at open (IncompatibleRepoFormat) instead of silently mis-reading everyTree/ChunkedBlob. Pre-1.0 API/format break, no migration. New normative specdocs/specs/SPEC-MERKLE-OBJECTS.mdpins the construction (#414). - BREAKING (
mkit-core): in theblamemodule, the public type aliasBlameResult2<T>was renamed toBlameOutcome<T>, and the unboundedmatch_linesfunction is now private — line matching is an internal, size-checked detail ofblame_file_with. Both are pre-1.0 API breaks; no in-workspace consumers were affected. (release-plz'ssemver_checkenforces the matching version bump at release time.) - BREAKING (
mkit-core): dropped.mkit/indexv1 read compatibility.deserializenow 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):Subjectgained a new required field,digest_sha256_hex, andsha256_hex()was added to compute it. Every subject now carries asha256digest alongsideblake3(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/SLSADigestSetsha256key) can read mkit attestations.sha2moves from optional (gated behindalgo-secp256k1/algo-p256) to an unconditionalmkit-attestdependency. Pre-1.0 API break; every caller constructing aSubject(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 atavailable_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 whytokio::task::spawn_blockingdefaults to 512 threads for the same class of work). Replaced with a benchmarkedMAX_SYNC_WORKERS = 64constant. Real effect on a 1000-fileadd/commit: mkit goes from ~1.6x slower thangit2(libgit2) to roughly matching it, while its lead over the actualgitCLI widens to ~21.5x. No change toWriteBatch'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 withPackfileTooLarge.remote_dispatch::build_and_upload_packsseals and uploads each pack as soon as it would exceed the cap (peak memory: one pack buffer, not the whole plan), andadvance_packmapnow chains an ordered set of pack keys onto a single packmap node instead of exactly one — the fetch side needed no changes, sinceresolve_pack_chainalreadyflat_maps every node'spacks. A push whose plan fits in one pack (the overwhelming common case) is unaffected. Also fixes a latent progress-reporting bug:PackUploadedbyte 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_treeno longer read a large file's entire contents into memory beforeFastCdc-chunking it — a newchunker::ChunkReaderstreams the content-defined cut algorithm off aReadsource, keeping at most one chunker window (256 KiB) resident regardless of file size (byte-identical boundaries to the in-memoryChunkIterator, pinned by proptest).worktree::hash_file_with_metadatais now the single streaming-capable entry point shared bymkit addandbuild_tree. Checkout got the matching fix:restore_blobstreams eachChunkedBlobchunk straight to the destination file via a newcreate_tmp_for_write/finish_atomic_writepair 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 -mracingcommiton the same branch could silently lose the commit. #637 serializedMatch-conditioned ref writes under a shared per-ref lock, butcommit's ref advance still usedRefWriteCondition::Any(an unconditional clobber) andbranch -m's delete of the renamed-away source ref was unconditional too — neither side went throughMatch, so the shared lock never engaged between them. A rename could read a branch's tip, let a concurrentcommitland 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'sadvance_headnow writesMatch(expected_tip)/Missinginstead ofAny(aborting with a clear, GC-recoverableTEMPFAILif the branch moved underneath it since composing the message), andbranch -mnow deletes the source ref via the new CAS-guardeddelete_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 inmkit-transport-memoryandmkit-transport-s3. A request for prefixrefs/heads/featincorrectly matchedrefs/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 normativelistRefsalgorithm; 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 servenow surface asRefConflict. Per SPEC-TRANSPORT §4.2.1 the server answers a compare-and-swap mismatch onupdateRefwithError{INVALID_REQUEST}carrying the current ref value inError.details; previouslymkit servecollapsed everyupdate_reffailure into a generic invalid-request with emptydetails, so a genuine conflict degraded toRemoteErroron 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 (sharedmkit_rpc::map_update_ref_error), so genuine invalid requests no longer misclassify as conflicts (#551). - External-signer
PinPrompt/PinResponseround trip is now implemented;mkit-sign-ctap --pinon 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, butExternalSigneronly wroteHello/SignRequestand rejected anyPinPromptframe withExternalSignerBadResponse— the only way to supply a PIN was the reference CTAP signer's plain--pinflag, readable by any other local user viaps//proc/<pid>/cmdline(the same exposure classdocs/THREAT-MODEL.md§3.2 defends key-file confidentiality against).ExternalSignernow keeps the child's stdin open for the whole sign conversation and answers aPinPromptvia a newPinProvidertrait (defaultTtyPinProvider: an interactive terminal prompt, best-effort no-echo viasttyon Unix — never argv or an environment variable), bounded to 8 round trips per conversation.mkit-sign-ctapnow requests a PIN in-band when the authenticator needs one and prints a deprecation warning to stderr when--pinis 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, CLIserve), and fixed several latent bugs (S3 ref pagination, atomic bisect-state write, BLS external-signer fail-closed) with regression tests.
Security
RUSTSEC-2025-0055ignore removed fromdeny.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 viacommonware-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 onlytracing-subscriberleft 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/fetchnow verify commit/remix/tag signatures and fail closed by default. Previously the only signature check was the manual, single-revisionmkit verify <rev>— a hostile remote (THREAT-MODEL §3.1) could push an entirely unsigned or forged history and everyclone/pull/fetchwould accept it with zero indication anything was wrong. Every commit/remix/tag a fetch newly introduces is now run throughmkit_core::sign::{verify_commit,verify_remix,verify_tag}— the exact checkmkit verifyruns 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 newDispatchError::UnsignedOrInvalidObjectis deliberately excluded from the applied-pack self-heal retry. Opt out per invocation with--no-verify-signatures, or persistently via the user-scopedpull.require_signed = falseconfig key (added toREPO_FORBIDDEN_KEYS— a cloned repo's own config cannot disable the check that protects the clone against exactly that repo). A new message-onlymkit.rpc.v1.verifyproto (verify.proto, co-located withsigner.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, andssh.identity_filekeys were parsed intoConfigbut never threaded into the spawnedssh(1)process, so the documented control was inert (a documented-but-absent security control). They are now mapped intoSshOptionsand passed to the child as-o StrictHostKeyChecking=… -o UserKnownHostsFile=… -i …, matchingdocs/SSH-SECURITY.md§3. Pure producer-side wiring — the transport already consumedSshOptions(#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-memoryEphemeralSink(no flush cost, no garbage objects, and no visible-but-unflushed object can poison content-addressed dedup —SyncPolicy::Noneremoved); 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-objectconfig key exposes the strict schedule; the index stat cache gained ino+ctime fields (catches replace-by-rename andtouch -r), a per-entry racy window for coarse-timestamp files, andstatusheals the cache from hash-time observations (never stat-after-verify) and never auto-upgrades a v1 index;stash poprecords the popped commit in the recovery log before dropping its manifest entry;status/diffsnapshots 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-memoryEphemeralSink, not a sync policy); flush counts and ordering are pinned by unit tests. Measured on an M4 Max (APFS):add+commitof a 100 MiB file 13.5s → 0.75s; 100 × 10 KiB files 1.1s → 0.18s. - Index v2 stat cache:
.mkit/indexentries now carrymtime_ns+size(SPEC-INDEX v2; v1 indexes read fine and upgrade on first write).add/status/commit -aprove unchanged files by stat instead of re-reading and re-hashing content — O(stat), with git's racy-clean rule applied at read time.statuswith an unchanged 100 MiB file: 113ms → 13ms. - Zero-copy ingest: chunk and small-blob writes stream
prologue ‖ payloadstraight from the source buffer (serialize::blob_prologue), eliminating two memcpys per chunk;status/diffsnapshots use an in-memoryEphemeralSinkand no longer pay any durability cost;worktree::hash_file_objectcontent-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-offgit-bridgefeature; #330).docs/specs/SPEC-GIT-IMPORT.mdpins 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 agit-import/v1attestation minted per head.mkit git pullfast-forwards or refuses with a nativemkit merge upstream/<branch>hint; force-pushes warn and rewind tracking refs only; deletions prune likegit 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-patchrenders native commits asgit am-able patches. Journeys indocs/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-offgit-bridgefeature; #330). New normative specdocs/specs/SPEC-GIT-BRIDGE.mdpins a byte-deterministic mkit→git object mapping (BLAKE3/SHA-1 translation with mkit-only fields — signer, signature, identity, annotation slots — carried inmkit-*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). Newmkit-git-bridgecrate implements the mapping with golden vectors underrust/tests/golden/git-bridge/, round-trip plus determinism plus differential-vs-real-git tests (git hash-objectid agreement,git fsck --strict). The exporter pushes with per-ref--force-with-leasefrom rebuildable state under.mkit/git/, skips untranslatable refs loudly (remix ancestry, git-illegal names, non-canonical chunking); the import direction is specified separately indocs/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;--repositoryconfines all calls; attestation predicate files must resolve inside the repo, trust-roots files outside it;--signer/--algorithmare always pinned explicitly so user config cannot reroute agent-triggered signing. Seedocs/CLI.md§"Agent integration".PinResponse.pinisdebug_redact([signer.proto]): generatedDebugimpls 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 MiBMAX_FRAME_BYTESsize cap at the protobuf decode itself (buffaDecodeOptions), inread_frameand 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_decodefuzz target: SignerFrame / SshFrame wire decode never panics on arbitrary bytes, plus anArbitrary-driven owned encode/decode roundtrip property (new opt-inarbitraryfeature on mkit-rpc, from buffagenerate_arbitrarycodegen). Adopts commonware-invariants minifuzz for therpc_decodetarget.mkitAgent Skill plus crates/docs MCP server — a published Agent Skill (SKILL.md) teaching agents to drive themkitCLI, plus a release-gated documentation MCP server indexing the source, SPEC docs, and CLI reference at each pinned release (#329, #331, #337).samplyprofiling support for the benchmark/profiling workflow (#347).
Changed
- Idiomatic buffa enum handling across all crates: raw
EnumValue::to_i32/as i32comparisons replaced withEnumValue's directPartialEqagainst enum values, and SHOUTY proto value names replaced with theUpperCamelCasealiases 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
*OwnedViewwrapper types,HasMessageViewimpls, and idiomaticUpperCamelCaseenum value aliases. The declared requirement is0.7.1(not0.7) because regenerated packed-view decoders call theRepeatedView::reservehook 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 themacos-keychainprotector but was opened with thesoftwareprotector — 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 towardkey.backendor<backend>:<label>routing. scripts/regen-rpc-proto.shcould silently copy stale staged sources back intogenerated/instead of fresh codegen output: the default build mode fills itsOUT_DIRwith the same.rsfile set, and the script picked whichever was freshest. Codegen mode now drops a.mkit-rpc-codegenmarker that the script requires.
Security
- New
git-bridge/v1attestation predicate (SPEC-GIT-BRIDGE §11; #330):mkit git exportmints 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 thegitCommitSHA-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 underrefs/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 indeny.tomland the audit workflow — it reaches the tree only transitively viacommonware-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 typetag(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.-acreates an unsigned annotated tag;-screates a signed tag whose signature is Ed25519 over the canonical tag bytes under a new, distinct signing domainmkit.tag\0(deliberately separate from the commit/remix domains to prevent cross-protocol signature reuse). Lightweightmkit tag <name>is unchanged.mkit verify <rev>now verifies signed tags (resolving a tag name to its tag object), andmkit catsurfaces 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 underrust/tests/golden/tags/. Specs:docs/specs/SPEC-OBJECTS.md§6a,docs/specs/SPEC-SIGNING.md§4a. mkit-keystorecrate — 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 indocs/specs/SPEC-KEYSTORE.md.mkit key …subcommand family —generate,list,import,export, anddeleteagainst any built-in keystore backend, with--backend/--label/--algorithmselectors and a--jsonoutput mode onlist.<backend>:<label>key-reference routing — commit signing, attestation signing, and themkit key …commands resolve their signing key through user-scopedkey.default_ref,key.ed25519_ref,key.secp256k1_ref, andkey.p256_refselectors. Repo-local config cannot override these for security reasons; the selector keys are accepted from$XDG_CONFIG_HOME/mkit/configand explicit flags only.mkit-rpccrate — shared length-prefixed framing and wire schemas (signer.proto,common.proto) used by the external signer subprocess protocol and reserved for future agent protocols. Seedocs/specs/SPEC-RPC.md.mkit status --porcelain=v1— machine-readable status output matching thegit status --porcelain=v1shape, plus the mkit- specificT(mode change) status letter as the only extension.mkit log --format=json— JSONL output (one commit per line) withhash,parents,tree,author,timestamp,title, andmessage.--format=jsononblame,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 arender-chartsbinary 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 inmkit-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-hookand exit withtempfail(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
Keystoreoperation 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 commitnow reads the staging index (.mkit/index) instead of recursively snapshotting the worktree. (#102) Pre-fix,mkit addandmkit rmwrote to the index butmkit commitignored it — a half-state that surprised any user reasoning by analogy from git. Post-fix,mkit add(ormkit add .) is required beforemkit commit; an empty index is now a hard error. The "snapshot the whole worktree" workflow ismkit 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_treeandbuild_tree_from_indexproduce 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, anda5b382c. - Silent failure exits in several subcommands now return proper sysexits-aware codes instead of exiting 1 with no diagnostic.
mkit commitindex follow-ups — preserve executable modes on-a/-am, stage tracked deletions onadd ., clear stale index path conflicts, and keep the index aligned with committed trees after PR #103 review.mkit rebasepreflights 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
mkitbinary, 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/configis 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-attestdefaults to$XDG_CONFIG_HOME/mkit/trust-roots.tomlrather than a repo-local path; in-repo trust-roots require an explicit--trust-rootsflag.- 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 enforced0700. - HTTP and S3 transports require an explicit user-scoped
trusted_remote_endpointbefore 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.