Sorrel Core
The Rust engine underneath everything: a content-addressed object store, snapshots and changes, lanes for parallel work, a real three-way merge with first-class conflicts, and a permission spine that works without any server.
Content-addressed objects
Every piece of history is an immutable object identified by the BLAKE3 hash of its
bytes. Objects live in a fanout store on disk (FileObjectStore) or in
memory for tests. Writes are atomic (temp file, then rename) and reads verify the
digest, so corruption is detected instead of propagated. Identical content always has
the identical id — on every machine.
| Object | What it captures |
|---|---|
Blob | Raw file bytes. |
Tree | A directory: named entries pointing at blobs and subtrees, with modes and sizes. |
Snapshot | A whole working tree at a point in time, plus parent snapshots — history is a DAG of snapshots. |
Change | An intent: author, message, base and resulting snapshot, and the path-level diff between them. |
Lane / Stack | Isolated streams of work (for humans or agents) and ordered stacks of changes, with owner, visibility, and policy references. |
Conflict / MergeResult | The outcome of a merge: stored, addressable, and inspectable — not an error string. |
Snapshots and changes
materialize_snapshot walks a directory into trees and blobs and writes a
snapshot. A stat cache (size + mtime) skips re-hashing unchanged files, which keeps
status-style operations fast on large trees. snapshot_diff
computes the path-level added/modified/deleted set between two snapshots, and
create_change records that diff as a Change object and links the
snapshots into history.
The merge model
Merging is engine work, not CLI string manipulation. Three layers cooperate:
-
Merge base:
merge_basewalks the snapshot DAG breadth-first from both sides and returns a deterministic best common ancestor (criss-cross ties resolve to the smallest id, so every machine agrees). -
Line merge:
merge3is a dependency-free three-way text merge. Regions changed on one side take that side; identical edits merge; overlapping different edits produce structured conflict hunks and Git-style<<<<<<<markers. Non-UTF-8 content is treated as binary. -
Snapshot merge:
merge_snapshotsmerges two snapshots against their base path by path. A clean merge writes a merged snapshot whose parents are both sides. A conflicted merge writes no snapshot — it storesConflictobjects (content, add/add, modify/delete, binary) and aMergeResultthat lists them.
Conflicts being real objects matters for agents: a conflicted merge is data another process can pick up, reason about, and resolve — possibly on a different machine, possibly by a different agent than the one that attempted the merge.
The policy spine
Core owns identity, permissions, grants, policy evaluation, redaction markers,
SecretRef semantics, and audit records — headless, with no server
required. Decentralized does not mean permissionless: policy and authority changes are
signed PolicyChange objects evaluated against the previous
effective policy. A principal cannot grant itself power it does not already have
delegated authority for; peers, runners, the Hub, and vaults reject forged or locally
edited permission state.
The contract is enforced across languages by protocol fixtures: a canonical
conformance manifest lives in sorrel-protocol and is vendored into every
consumer (engine, CLI, Hub, runners, vault) with drift-guard tests, so all evaluators
provably return the same decisions.
Performance stance
- BLAKE3 ids; stat-cache to avoid re-hashing unchanged files.
- Engine-level exclusion (e.g.
.sorrel/) instead of tree copies. - Dependency-free benches with coarse budgets that fail on order-of-magnitude regressions.