1. Overview
The product promise is operational zero-knowledge in the SaaS sense: the operator routes and stores opaque ciphertext and signed metadata, but does not possess plaintext repository data or repo content keys. The trust boundary is the user's signed native binary — a browser session is a convenience tier, not a root of trust.
What stays on the client
- Git pack/object plaintext, refs, commit messages
- Repo content keys and their epoch wraps
- Device signing & wrapping private keys
- Personal recovery kit material
- Decrypted pull request and review state
What the server sees
- Encrypted chunks and signed manifests (opaque ciphertext)
- Signed append-only event log with parent-hash chaining
- Public device and runner signing keys (per repo)
- Opaque repo / chunk / object IDs and ciphertext sizes
- Account billing / membership edges (acknowledged leakage)
System-level diagram
2. Trust model
hushgit assumes an active server-side attacker that can read, modify, reorder, replay, or equivocate over all server-side data. Confidentiality therefore depends on client-side encryption and integrity depends on signed writes plus client-pinned state. There are four distinct trust tiers in the product:
| Tier | What it can do | What it can never do |
|---|---|---|
| native trusted CLI, sidecar | Sign repo events, open encrypted review payloads, hold device wrapping keys, approve enrollments, rotate keyrings. | Skip native bootstrap or impersonate another device — signatures bind signer_device_id. |
| browser convenience web, desktop renderer | OPAQUE login, list opaque repos, fetch encrypted event ciphertext, render activation prompts. | Approve enrollment, create a repo, upload/delete chunks, append signed events, decrypt review payloads. |
| recovery personal kit | Sign one narrow RecoveryDeviceAdd action; receive new repo-content-key epoch wraps. |
Push, comment, approve, or sign arbitrary keyring changes. |
| runner repo-scoped CI | Sign job claim, heartbeat, completion, and runner audit events for its repo; under an active lease, sign job-scoped fetches of the encrypted job spec, signed manifest, and pack chunks, and upload encrypted result chunks. | Approve enrollment, create repos, upload/delete arbitrary chunks, mutate refs, or change keyrings. |
| untrusted server, storage, CDN, network | Apply CAS preconditions and rate limits; serve opaque blobs and signed event pages. | Decide Git ancestry, branch protection semantics, or read plaintext metadata. |
Origin, Sec-Fetch-*) and by
deploying the web app on a distinct origin from the API. CORS gates and origin separation
are browser-execution hardening, not authority checks — the actual authority is the
native device signature.
3. Workspace layout
The monorepo is split by language and by what owns the security-critical path. Everything that touches keys, signatures, or ciphertext is in Rust. The TypeScript surface is the convenience UI plus a thin SDK that mirrors the protocol types.
. ├── apps/ │ ├── hushgit-cli/ Rust CLI: keys · auth · repo · pr · doctor │ ├── hushgit-server/ Rust HTTP API (Axum + sqlx + S3) │ ├── hushgit-runner/ Rust self-custodied CI runner │ ├── hushgit-desktop-sidecar/ Rust IPC sidecar — native authority for Electron │ ├── git-remote-hushgit/ Rust Git remote-helper entrypoint │ ├── web/ Vite + React encrypted repo/PR browser (convenience) │ ├── desktop/ Electron renderer + main process │ └── landing/ Astro static site, waitlist on Cloudflare D1 ├── crates/ │ ├── hushgit-core/ Client orchestration (device store, sessions, remotes) │ ├── hushgit-crypto/ Envelopes, ChaCha20Poly1305, Ed25519, X25519 │ ├── hushgit-events/ Signed event DAG, manifest, merge-cert verifiers │ ├── hushgit-git/ Hardened shell-out to the git CLI, refs/packs/bundles │ ├── hushgit-keys/ Key hierarchy, keyring draft, recipient models │ ├── hushgit-native-client/ Account/device flows requiring native custody │ ├── hushgit-protocol/ Shared wire types: events, manifest, keyring, HTTP, CBOR │ ├── hushgit-server-client/ Hardened HTTP client for server calls │ ├── hushgit-storage/ ChunkStore trait + local backend │ └── hushgit-wasm/ Future browser bindings (placeholder) ├── packages/ │ ├── sdk/ TS protocol mirror + browser HushgitClient │ └── ui/ Shared React components (PR, shell, status) ├── api/openapi.yaml Pinned HTTP contract ├── docs/ Threat model · whitepaper · ADRs · this file ├── infra/dev/ Docker compose for local Postgres + MinIO └── xtask/ Rust workspace maintenance
4. Layered architecture
Crates form a strict three-layer stack. The foundation owns wire types and primitive crypto. A middle orchestration layer composes those primitives into device stores, keyrings, sessions, and remotes. Apps sit at the top and are the only place where I/O, argument parsing, and process boundaries live.
hushgit-core to verify signed manifests and materialize job checkouts. Full client orchestration lives in hushgit-core, the hub used by every native client.5. Crate dependency graph
Concrete inter-crate edges. Foundation crates have no inbound dependencies from
other hushgit crates and are deliberately small. The hub is hushgit-core,
which every native app links against.
protocol, crypto, and events, deliberately skipping client orchestration so it cannot accidentally pick up client-side state. The runner links protocol, crypto, hushgit-core, and hushgit-native-client; core supports signed-manifest verification and checkout materialization, while native-client supplies shared transparency verification helpers. Hover a crate to isolate its edges; the table below is the exhaustive edge list.Edge summary
| Crate | Depends on (hushgit-* only) | Imported by (hushgit-* only) |
|---|---|---|
hushgit-protocol | — | keys, events, git, server-client, native-client, core, transparency, server, runner, cli, git-remote-hushgit, desktop-sidecar, witness |
hushgit-transparency | crypto, events, protocol | native-client, server, cli, git-remote-hushgit, witness |
hushgit-crypto | — | keys, events, core, native-client, transparency, server, runner, cli |
hushgit-git | protocol | core, cli |
hushgit-storage | — | core, git-remote-hushgit |
hushgit-server-client | protocol | native-client, cli, desktop-sidecar |
hushgit-keys | crypto, protocol | core |
hushgit-events | crypto, protocol | core, transparency, server, cli, git-remote-hushgit, desktop-sidecar |
hushgit-core | crypto, events, git, keys, protocol, storage | native-client, cli, git-remote-hushgit, desktop-sidecar, runner |
hushgit-native-client | core, crypto, protocol, server-client, transparency | runner, cli, git-remote-hushgit, desktop-sidecar |
Checked against each crate's runtime [dependencies] by cargo run -p xtask -- check-architecture-deps
(part of make check); dev-dependencies are not edges here. Note that hushgit-runner depends on
protocol, crypto, core, and native-client (signed-manifest verification, checkout
materialization, and shared transparency verification); it does not link hushgit-events, so its event verification helpers come
from local hushgit-protocol payload helpers plus hushgit-crypto signatures rather than the events crate.
6. Cryptographic envelopes
Every byte that crosses the trust boundary is wrapped in a single envelope type. The envelope carries the algorithm, purpose, and key identifier as plaintext fields and binds them as authenticated associated data, so the server cannot strip the header or cross-mount ciphertext between purposes.
EncryptedEnvelope (crates/hushgit-crypto/src/envelope.rs)
pub struct EncryptedEnvelope {
pub version: u32, // currently 1
pub algorithm: String, // "ChaCha20Poly1305+BLAKE3-Derive/v1"
pub purpose: CryptoPurpose, // domain separator (enum)
pub key_id: String, // logical key id, e.g. "repo-key-v1"
pub nonce: Vec<u8>, // 12 random bytes
pub ciphertext: Vec<u8>, // AEAD ciphertext + 16-byte tag
}
The algorithm string is constant in v1: ChaCha20Poly1305 for AEAD and BLAKE3-derive for purpose-domain key derivation. The parser rejects unknown algorithm strings before any crypto runs, so an attacker cannot downgrade the wire format.
CryptoPurpose (crates/hushgit-crypto/src/purpose.rs)
pub enum CryptoPurpose {
Chunk, // encrypted Git pack chunk
Manifest, // signed encrypted manifest
Refs, // reserved
PullRequest, // PR payload
Comment, // review comment
EventPayload, // generic event payload
Keyring, // encrypted keyring snapshot
DeviceStore, // local on-disk device key store
}
Each purpose derives a distinct purpose-key from the base key, so the same repo content key produces eight independent encryption keys. A chunk ciphertext cannot be replayed as a manifest or comment without rejecting on AAD mismatch.
7. Key hierarchy
Every repository has a single 32-byte symmetric content key per epoch. The keyring
WrappedRepoKey list wraps that key to each authorized device and each active
personal recovery key (the WrappedRepoKeyRecipient enum also includes
User and Org variants for future per-user / per-org wrapping). The
server stores wrap blobs but never the content key itself.
WrappedRepoKeyRecipient enum supports Device, PersonalRecovery, User, and Org — there is no Runner recipient. Approved runners receive repo-key material only through owner-controlled native-client handoff.Device key shape
Every device holds two distinct Ed25519/X25519 keypairs:
- Signing keypair (Ed25519). Signs repo events, signed manifests, keyring changes, chunk uploads, and runner approvals. The public key is recorded in the keyring's
authorized_devicesand inrepo_signing_keyson the server. - Wrapping keypair (X25519, static). Used to wrap the repo content key with
X25519-HKDF-SHA256+ChaCha20Poly1305/v1(seeREPO_KEY_WRAP_ALGORITHM). The public key is recorded in the keyring; the private key never leaves the device.
A device id is bound to its signing key — rotating signing material requires provisioning
a new device, not re-registering the same id. Convenience accounts hold a
browser_convenience Ed25519 key used only to identify the browser session for
account-scoped reads.
8. Keyrings & epoch rotation
Keyring state is the authorization root for a repository. It is modeled as a signed encrypted snapshot plus a linear log of signed keyring-change events. Clients never trust a snapshot in isolation — they replay the change log from a locally pinned trusted head and compare the replayed state to the candidate snapshot before using any device or recovery signer.
EncryptedKeyring (snapshot)
pub struct EncryptedKeyring {
pub signature: Vec<u8>,
pub signer: KeyringSigner,
pub version: u32,
pub repo_id: RepoId,
pub sequence: u64,
pub keyring_epoch: u64,
pub key_epoch: KeyEpoch,
pub authorized_devices: Vec<AuthorizedDevice>,
pub revoked_devices: Vec<RevokedDevice>,
pub wrapped_repo_keys: Vec<WrappedRepoKey>,
pub keyring_event_head_ids: Vec<EventId>,
pub recovery_policy: Option<RecoveryPolicy>,
}
KeyringChangeDetails (log entry)
- InitialKeyring (wire name
genesis_keyring) — genesis state, signed by the first native-trusted device, carries initialauthorized_devices,revoked_devices, andrecovery_policy. - DeviceAdd — appends one
AuthorizedDevice; signed by a currently authorized device. - RecoveryDeviceAdd — narrow add signed by a personal recovery key, carrying the
recovery_key_idplus the newAuthorizedDevice. - DeviceRevoke — appends one
RevokedDevice; in practice paired with a fresh content-key epoch and rewraps. - EpochRotate — pure rotation marker without a membership change.
Recovery-policy edits are not a keyring-change variant in v1; the policy carried by InitialKeyring is the only signed policy surface in the change log today. The policy_update RepoEventKind is a separate event for protected-branch policy and lives outside this enum.
9. Signed event log
Every mutating action is a SignedEvent appended to a per-repository
append-only log. Events form a parent-linked DAG so clients can detect truncation,
reordering, replay, and equivocation. Runner audit lives in a separate per-repo
repo_runner_events stream that may reference repo events as parent hints.
SignedEvent (events/repo.rs)
pub struct SignedEvent {
pub version: u32,
pub event_id: EventId,
pub repo_id: RepoId,
pub actor_user_id: UserId,
pub signer_device_id: DeviceId,
pub kind: RepoEventKind,
pub encrypted_payload_chunk_id: Option<ChunkId>,
pub parent_event_ids: Vec<EventId>,
pub created_at_unix_ms: i64,
pub signature: Vec<u8>,
}
RepoEventKind
pub enum RepoEventKind {
Push,
RefUpdate,
PolicyUpdate,
PullRequestCreate,
PullRequestUpdate,
CommentCreate,
CommentUpdate,
CommentDelete,
ReviewSubmit,
MergeIntent,
MergeCertificate,
KeyringChange,
}
created_at_unix_ms is owner-supplied
display metadata, not a trusted clock. The server's append sequence and event ids are the
authoritative ordering; clients may detect equivocation by pinning event heads after every
sync.
10. Signed manifest
A signed manifest is the per-repository state snapshot: refs, pack chunks, the keyring epoch in force, and the content-key epoch used to seal everything inside the envelope. Manifest validation pins all three so an attacker cannot replay an old manifest under a newer keyring or pair new refs with an old keyring.
Protected branch policy
Protected-branch policy bytes are signed as part of the encrypted manifest. The server stores signed events, encrypted chunks, packs, and the signed manifest under CAS, but it does not decide Git ancestry or whether a merge satisfies policy. Clients validate protected refs against the manifest:
- required_approvals — distinct eligible approving users, excluding the pull request author and merge certificate signer.
- direct_push —
Deny/AllowAll/Allow{ users[], devices[]? }. - protected_from_object_id — pins the object that protection started from, so advancement that bypasses the signed update log is detectable.
- MergeCertificate — detached signature over PR id, source/target/result object ids, pull request author, approval event ids (up to 64), signer device, timestamp. Protected refs advance only via a valid cert or an explicit policy-authorized direct push.
11. End-to-end push & fetch
The Git remote helper (git-remote-hushgit) is the only place where Git's
transport talks to the network. git push hushgit::<url> main ends up
generating an encrypted pack chunk, signing the upload, signing a Push event,
and CAS-updating the manifest.
Fetch reverses this: the helper fetches the manifest, validates its signature and bindings,
validates the keyring change log up to keyring_event_head_ids, downloads
referenced encrypted chunks, decrypts them locally, and imports the resulting pack into a
temporary mirror that Git fetches from. Ciphertext reads are themselves authenticated:
chunk and repo-object GETs carry a timestamped fetch statement signed by an active
native-trusted repo signer (x-hushgit-chunk-fetch-signature /
x-hushgit-repo-object-fetch-signature), so a session bearer alone cannot
bulk-download ciphertext.
12. Server storage model
The server is split into two planes: a relational metadata plane in Postgres (managed by
sqlx migrations under apps/hushgit-server/migrations/) and a content plane on
an S3-compatible blob store. All values that cross the trust boundary are opaque ciphertext
or signed envelopes; the server never sees plaintext.
HTTP surface, grouped
| Group | Routes (selected) | Notes |
|---|---|---|
| Auth | register/start, register/finish, login/start, login/finish, recovery/start, recovery/finish, devices, devices/{id}/revoke, native-bootstrap, device-enrollments, browser/device-enrollments (read-only), session |
OPAQUE PAKE; password material never reaches the server. Native bootstrap consumes a one-time OPAQUE login finalization. |
| Repos | POST /repos/challenges, POST /repos, GET /repos, DELETE /repos/{id} |
Creation requires a server-issued one-time challenge signed by a native-trusted device. Deletion needs confirmRepoId and cascades blob deletions. |
| Chunks | GET /repos/{id}/chunks/{chunkId}, PUT …, DELETE …, POST …/delete-challenges |
Upload signature binds repo id, chunk id, ciphertext sha256. Fetch requires a timestamped fetch statement signed by an active native-trusted repo signer. Deletion is challenge-bound to chunk generation. |
| Repo objects | GET /repos/{id}/objects/{objectId}, PUT … |
Manifests and review payloads. Writes take a precondition — absent (If-None-Match) or matches-sha256:{etag} (If-Match) — bound in the signature; reads require a signed fetch statement, as with chunks. |
| Events | GET /repos/{id}/events, append via signed-event POST |
Owner-scoped cursors are navigation markers, not authorization tokens. |
| Runner pairing & admin | POST /repos/{id}/runner-registrations, …/{regId}/claim, …/{regId}/approve, GET /repos/{id}/runners, POST …/runners/{runnerId}/revoke |
15-minute pending TTL; 40-bit pairing code stored only as a peppered hash; bounded online attack budget. Revocation is owner-signed, idempotent, and future-authority-only. |
| Runner jobs | POST /repos/{id}/runner-jobs, GET …/runner-jobs, POST …/runner-jobs/claim, …/{jobId}/heartbeat, …/{jobId}/complete, …/{jobId}/cancel, …/runner-events |
Create, cancel, and list are owner-session routes with owner-signed payloads. Claim, heartbeat, complete, and runner-event append are bearer-less and authenticate by verifying the runner signature against the active approved runner key for that repo. |
| Job material & results | POST …/runner-jobs/{jobId}/spec-chunk, …/{jobId}/chunks/{chunkId}, …/{jobId}/repo-objects/{objectId}, PUT …/{jobId}/result-chunks/{chunkId} |
Lease-gated, runner-signed, job-scoped: fetch the encrypted job spec, pack chunks, and signed manifest; upload encrypted result chunks. All payloads stay ciphertext to the server. |
13. Self-custodied CI runner
The first CI product is bring-your-own-machine. The runner is a repo-scoped principal, not an account device — its signing key authorizes job claim, heartbeat, completion, and runner audit events, plus lease-gated job-scoped fetches of the encrypted job spec, signed manifest, and pack chunks, and encrypted result-chunk uploads. Repo key material reaches the runner only through owner-controlled handoff: the owner CLI exports a repo content key file that the operator places on the runner host — the runner is never a keyring recipient. With that material the runner verifies the owner-signed manifest and materializes a checkout in an isolated workspace before executing the job.
| Phase | Signed payload includes | Server-side bounds |
|---|---|---|
| Intent (owner) | random UUIDv4 registration id, native device id, ts | 15-min staleness, per-repo pending cap |
| Claim (runner) | pairing code, runner signing + wrapping public keys, ts | 5/burst + 60s refill, 50 lifetime per registration, 40-bit code |
| Approve (owner) | exact runner pubkeys + key id, pairing code, ts | 15-min staleness, single-use per registration |
| Job claim | repo id, runner id, lease_duration_ms, ts | 2-min staleness; server-enforced lease range [1s, 900s] (the runner CLI further requires ≥60s); replay returns original attempt |
| Material fetch | repo, runner, job, attempt, lease id, exact chunk / object id, ts | 2-min staleness; lease must be active and the runner approved; serves the encrypted job spec, signed manifest, and pack chunks as ciphertext only |
| Heartbeat | repo, runner, job, attempt, lease id, status, ts | monotonic, cannot shorten lease, older heartbeats rejected |
| Complete | repo, runner, job, attempt, lease id, result (success / failure), ts, result chunk id? | accepted only while lease active and runner approved; exact retry idempotent |
| Cancel (owner) | user id, repo id, job id, authorizing device id, ts | 15-min staleness; owner session plus an active repo-signer signature; replay idempotent; running attempts observe the terminal state at their next poll and stop |
| Revoke (owner) | repo id, runner id, ts | 15-min staleness, idempotent, future-authority-only |
.env next to the config.
14. Trust-tier matrix
Concrete summary of which client tier can do what. Anything outside this matrix is by construction either client-side computation or denied by the protocol.
| Capability | CLI (native) | Sidecar (native) | git-remote-hushgit | Runner | Desktop renderer | Web |
|---|---|---|---|---|---|---|
| OPAQUE login (account auth) | ✓ | ✓ | via session | — | via sidecar | ✓ |
| Hold device signing private key | ✓ | ✓ | via profile | — | — | — |
| Native bootstrap (mint native trust) | ✓ | ✓ | — | — | — | — |
| Approve device enrollment | ✓ | ✓ | — | — | — | — |
| Create / delete repo | ✓ | ✓ | — | — | — | — |
| Sign chunk upload / event append | ✓ | ✓ | ✓ | runner events · job results | — | — |
| Open encrypted review payload | ✓ | ✓ | — | — | — | — |
| Sign merge certificate | ✓ | ✓ | — | — | — | — |
| Rotate / revoke keyring | ✓ | ✓ | — | — | — | — |
| Claim runner job, heartbeat, complete | — | — | — | ✓ | — | — |
| List opaque catalog / pending enrollments | ✓ | ✓ | — | — | read | read |
| Fetch encrypted event ciphertext | ✓ | ✓ | ✓ | — | read | read |
| Fetch encrypted chunks / repo objects | ✓ | ✓ | ✓ | job-scoped | — | — |
✓ allowed and authoritative · "read" allowed but ciphertext-only · "via sidecar"/"via session" delegated through native authority · "runner events · job results" the runner key signs runner job/audit routes and job-scoped result uploads · "job-scoped" fetch statements bind job, attempt, and lease ids and require an active lease
15. Threat-model notes worth memorising
The server is treated as actively malicious
It can read, modify, delete, reorder, or replay any byte it stores. It may also attempt equivocation — showing different event histories to different users. Mitigations: signed manifests, signed event log with parent-hash chaining, client-pinned heads, and snapshot replay against signed change logs.
Browser is convenience, not trust
Browser-delivered JavaScript is untrusted unless build transparency/reproducibility is solved. Browser sessions can read opaque catalog and ciphertext but cannot receive payload-opening keys, cannot create repos, and cannot sign repo events. CORS + browser-execution-header rejection enforce origin separation; the actual authority is the native device signature.
Revocation is future-access-only
A revoked device retains any plaintext or key material it already received until the repo is rotated. Repository content-key rotation creates a new epoch for future writes but preserves old epoch wraps so authorized historical readers still work — true forward secrecy requires history rewrite or re-encryption.
Native bootstrap is a one-shot trust window
Before any native-trusted device exists, password compromise plus access to the OPAQUE login flow is sufficient to mint the first native-trusted device. After native trust exists, additional devices require approval from an existing native-trusted device or a personal recovery key. Account recovery codes reset password access; they do not authorize a Git signing device.
Protected branches are client-enforced
The server stores signed events, encrypted chunks, packs, signed manifests, and CAS preconditions — it does not decide ancestry or whether a merge certificate semantically satisfies policy. A malicious server can withhold, fork, or try to advance encrypted state incorrectly; clients detect this on signed-manifest load and refuse protected refs without a valid direct-push authorization or merge certificate.
Personal recovery kit is one factor
The encrypted kit file plus the 256-bit recovery code together unwrap any repo content-key epoch wrapped to the recovery key and sign a narrow RecoveryDeviceAdd. They must not authorize ordinary repo writes or arbitrary keyring changes. Store kit and code separately; rotate or revoke after suspected exposure.
Acknowledged MVP leakage
Org and account existence, billing/membership edges, push/pull frequency, timing, approximate ciphertext sizes, and network metadata are observable. Not leaked: repo names, branch names, file paths, commit messages, PR titles/descriptions/comments, review decisions, plaintext CI logs (when stored by the customer runner).
Equivocation defense is enforced for current server state
Current server-backed account, repository, runner, catalog/list, and byte-body clients verify witnessed checkpoints, subject-map proofs, private openings, local-pin consistency, and client-verifiable operation fields before exposing trusted state. Release and web build transparency, plus future inventory or pagination APIs, must add their own proof-bound semantics before they join that claim.
16. Glossary
| Term | Meaning |
|---|---|
| AAD | Authenticated Associated Data — header fields bound into the AEAD tag so they cannot be modified without invalidating decryption. |
| CAS | Compare-and-set. Used on object PUTs via If-Match/If-None-Match to detect concurrent updates. |
| ChunkId vs ChunkKey | ChunkId (in hushgit-protocol) is the logical identifier shaped like chk_<32-hex-chars>; ChunkKey (in hushgit-storage) is the backend storage key used by the ChunkStore trait. The server's repo_chunks row maps logical chunk id → blob key. |
| Convenience device | A browser-only Ed25519 key created during web signup. Identifies the session but cannot sign repo events or approve enrollments. |
| Device id | Opaque identifier bound to a specific signing keypair. Rotating signing material requires a new device id. |
| EncryptedEnvelope | Universal AEAD container: version, algorithm, purpose, key id, nonce, ciphertext. AAD-binds repo and object identifiers. |
| Epoch (key) | The integer version of a repo content key. Increments on rotation; old epochs retained for historical reads. |
| HUSHGIT_HOME | Per-OS-user directory holding profile metadata and device-store roots. Created with 0o700 permissions. |
| Keyring | Signed snapshot of authorized devices, revoked devices, wrapped repo keys, recovery policy, and epoch pointers. Cached materialized view of the keyring-change log. |
| MergeCertificate | Signed object binding a PR id, source/target/result object ids, approval event ids, and signer device. Required for advancing a protected ref via merge. |
| Native bootstrap | The one-shot OPAQUE-finalized flow that mints the first native-trusted device for an account. |
| OPAQUE | Augmented PAKE used for account registration and login. The server never sees raw passwords or hashes of raw passwords. |
| Personal recovery kit | Locally encrypted file plus 256-bit code that holds repo-scoped recovery wrapping + signing keys. Single-factor offline credential. |
| Profile | Local credential unit for one account on one server. Carries a stable profile_id, session revision, and credential-store account identifier. |
| RepoEventKind | Closed protocol enum of mutating action types (Push, RefUpdate, PolicyUpdate, PullRequest*, Comment*, ReviewSubmit, MergeIntent, MergeCertificate, KeyringChange). |
| TrustedRemote | Client-side abstraction over a hushgit remote (local filesystem or server-backed). Owns manifest decoding, keyring validation, and chunk fetch/store. |