A composition root is the sole place all four seams — RefStore, the
object store via gix_object::Find / Exists / Write, EventSink,
Executor — are wired together, applying <<arch.store-composition-root>>
and <<arch.no-hosted-branch>> across the full seam set, not just store
implementations.
docs/spec/roots.adoc
Composition Roots
No code inside the library knows where it is running; the core is handed
trait objects and never asks.
Deployment exists only in composition roots: each wires the four seams —
RefStore, an object store via gitoxide traits, EventSink, and
Executor — in roughly fifty lines of trait construction.
An if hosted branch inside the library is the design failing.
The git-ents CLI’s composition root MUST wire: a loose-ref RefStore,
the local odb as object store, a Docker Executor, a null EventSink, and
the advisory gate (<<gate.advisory-local>>).
git ents serve MUST reuse this same wiring, adding only the ents-web
HTTP frontend on loopback — the web UI, never git’s smart-HTTP wire
protocol, which the local root MUST NOT expose.
Local effect execution MUST be pull, via git effect run, never a daemon
watching refs; the queue is the only component this root skips
(<<effect.local-run>>).
The git-ents binary’s second composition root (the CLI-complete
milestone deployed at git.ents.cloud) MUST wire: a loose-ref RefStore,
a real on-disk object database, an in-memory EventSink scoped to one
hook invocation and reconciled at boot (<<receive.reconstructible>>), a
Fly.io Sprite Executor, and the mandatory gate (<<gate.mandatory-hosted>>).
Unlike <<roots.local>>, git’s own receive-pack is the transport and
performs the actual object unpack and ref update; this root’s
pre-receive/post-receive hooks call the gate and reconcile obligations
around that write, never through this crate’s own RefStore::transaction,
to avoid a double-write race against receive-pack’s internal ref update.
This root MUST also mount the web UI: the same `ents-web frontend the
local root serves (<<roots.web-agnostic>>), wired over this root’s own
seams with the mandatory gate and the server’s member key as signing
identity (<<roots.web-signing>>), served behind the same host.
The front proxy MUST route git’s smart-HTTP paths to stock git’s
http-backend and every other path to the loopback-bound web process;
the web process MUST NOT add a git transport of its own.
This is not <<roots.hosted>>: that root replaces the RefStore and object
store themselves with Postgres and Tigris once scale forces receive-pack
out entirely (<<roots.honesty-test>>); this root keeps a real on-disk
repository and stock git as the one hosted and local serving transport
until that phase — "stock git wearing the same gate everything else runs,
not a bespoke protocol."
The git-ents-server composition root MUST wire: a Postgres RefStore,
Tigris as object store, a durable queue as EventSink, a Fly.io Sprite
Executor, and the mandatory gate (<<gate.mandatory-hosted>>).
The worker (queue consumer, executor, push-back client) MUST stay a mode
inside the git-ents-server binary until scale forces a split, and MUST
NOT share in-process state with receive (<<receive.unit>>).
Wiring the Postgres RefStore, Tigris object store, and durable-queue
EventSink (<<roots.hosted>>) as a new composition root MUST require
zero modification to any library crate.
This is the project’s honesty test for the seam design: the proof is an
actual production migration off the single-node hosted root
(<<roots.single-node-hosted>>), not a synthetic root built to demonstrate
the seams.
Configuration MUST select trait implementations only at the composition root and MUST NOT leak past it; core code reading configuration directly is a missing trait, not a shortcut to take.
A hosted web edit MUST be signed by the server’s own member key, and that key MUST itself be an enrolled member. A hosted web edit’s commit MUST be authored by the signed-in member (<<roots.web-signin>>) and committed by the server identity (<<receive.attributed-author>>), so history reads "member via the web" — the member is never impersonated as the signer. The local web UI MUST sign edits with the user’s own member key instead; the server-key indirection used hosted MUST NOT be imported into the local root.
The ents-web crate MUST receive its signing identity by injection from
its composition root and MUST NOT assume it is reached over a network.
In-process webview embedding, with no network transport at all, MUST
remain a supported deployment alongside hosted and local-loopback serving.
|
Note
|
Example: policy lets Joey write |
Pushing into a local repository’s checked-out branch from outside is
integration-test-harness capability, not a product-facing feature: the
local root’s user-facing serving surface is the web UI
(<<roots.web-signing>>), never a git transport.
For that harness case, the git-ents local root MUST set
receive.denyCurrentBranch=updateInstead so an accepted branch push
also updates the working tree.
Worktree update MUST happen only after receive accepts the push, as
frontend business; core code MUST NOT touch a worktree.
A dirty worktree is a known edge where updateInstead fails, so this
harness path differs from hosted while metadata behavior, which never
touches a worktree, stays identical.
A hosted web session MUST be held only in server memory, consistent with <<model.account>>'s ban on a session database. Every state-changing web request MUST carry a per-session CSRF token the server verifies before acting; a web edit MUST be signed (<<roots.web-signing>>) only on behalf of an authenticated session.
A hosted web session MUST become authenticated (<<roots.web-session>>) only by proving control of an enrolled, active member key: the server issues a one-time challenge bound to exactly one browser session, and the member’s own tooling signs it under an SSHSIG namespace distinct from git’s commit-signing namespace, so a sign-in signature can never double as a push signature or vice versa. A challenge MUST be short-lived and single-use; consuming it MUST be the only path to authenticating the session it was bound to. The signed payload MUST bind the serving host and the challenge itself, so a signature cannot be replayed against another deployment or another session, and the signing tool MUST construct that payload locally from the host the member addressed, never from bytes the server supplies. An authenticated session MUST record only the member’s username and public key, held in server memory per <<roots.web-session>>; no member secret is ever transmitted or stored. An unauthenticated hosted session MAY browse read-only and MUST NOT reach any signing path (<<roots.web-signing>>); a mutation under a session whose member is no longer enrolled and active MUST be refused at the time of the mutation, not only at sign-in. The local root (<<roots.local>>) MUST NOT expose a sign-in surface: its serving identity is the operator’s own key, resolved at startup.
The hosted root MUST validate every repository-path segment before any filesystem or subprocess use, rejecting a path that would escape the data directory, nest inside an existing repository, or collide with a non-repository namespace directory.
Fetch authorization MUST be refname-keyed, using the same authorization model as write authorization. Private-repository access beyond refname-keyed fetch authorization is out of scope for v1.
A hosted repository is created on its first info/refs request, before
any signed push exists, which leaves "first push owns the repo" open per
repository (<<gate.bootstrap>>).
Closing it MUST require a server-level key-to-account registry gating
repository creation; this direction is chosen but not yet enforced.
Every composition root through the single-node hosted root
(<<roots.local>>, <<roots.hosted>>) serves fetches and clones via stock
git’s own upload-pack acting directly on a real on-disk object
database, which has no knowledge of refs/meta/redactions/* and will
serve a redacted object’s bytes to any fetch if they are already
reachable in that odb.
receive’s ingest-time check (<<receive.redaction-ingest>>) only
refuses a redacted hole being refilled by a new push; it cannot evict
bytes already stored before the redaction record existed, nor bytes a
fetch already served.
`gix-receive (<<roots.honesty-test>>) replaces only incoming pack
ingestion at the scale-out root; nothing in this design wraps outgoing
pack generation for any root.
Making pack generation redaction-aware, or otherwise physically evicting
redacted bytes from an on-disk repository, is chosen direction but not
yet designed.
git-ents-server MUST be a library first: the serve command, the
standalone binary, and the git hooks MUST be thin wrappers over that
library, not independent implementations.
This MUST keep authorization (<<gate.tip-signed>>) and effect-matching
(<<receive.event-sink>>) logic inside the library, never smeared across a
subprocess boundary.