git-ents.gitmain
⌘K
foforge

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.

Deployment Lives Only at the Root

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.

git-ents Local Root

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>>).

git-ents Single-Node Hosted Root

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."

git-ents-server Hosted Root

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>>).

Scale-Out Root Is the Honesty Test

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.

Config Selects, Never Leaks

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.

Web Edit Signing Indirection

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.

Web UI Is Deployment-Agnostic

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 joey/* and refs/meta/issue/*. He starts the local UI (<<roots.web-signing>>). A commit to an issue, or to joey/*, shows green. A commit to main shows orange or red — "when you push, the server will reject this: your signing key is not authorized for refs/heads/main" (<<gate.verdict-reason>>) — with an offer to send it to his inbox instead (<<sync.inbox-routing>>). Nothing was ever blocked locally: every verdict is a prediction of the gate, evaluated against policy as of Joey’s last fetch (<<gate.policy-as-state>>), rendered by the same in-process signing this section specifies — never a separate git-serving transport. The refs/heads/main case is aspirational as stated: it is a transport-level check, not the tip invariant (<<gate.principled-split>>), and this spec does not yet define that mechanism (<<gate.branch-acl-undefined>>), so the local UI has no policy to predict against for it today.

Test-Harness Pushes Into a Working Tree

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.

Hosted Web Sessions

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.

Hosted Web Sign-In Proves a Member Key

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.

Repository Path Validation

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

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.

Repository Creation on First Push (Deferred)

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.

Outgoing Pack Serving Ignores Redaction (Deferred)

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.

Embeddable Server

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.