git-ents.gitmain
⌘K
foforge

The Gate

Verification is a pure function over ref-store reads: it MUST depend only on the read half of the RefStore seam (<<arch.refstore-read-cas-split>>), never on write access or on any state outside refs/meta/*. The gate is the one admission judgment in the design; every mutation, human or machine, is admitted or refused by this same function (see <<receive.unit>>).

Verification

Tip Signature

The new tip of a meta-ref MUST be signed by a member authorized for that refname, judged against the member entity in force at acceptance time — the member ref’s tip in the same snapshot the gate reads (<<model.member-revocation>>). Anyone with a clone can run the same judgment against the same snapshot, not only whoever ran the server; reconstructing what a past acceptance saw is an audit function over the deployment’s out-of-scope op log, not a gate path.

Identity Binding

The refname being updated MUST be recomputable from the proposed tip’s signed content, per namespace exactly as <<meta-ref.identity-binding>> tabulates; a mismatch MUST refuse. Without this check a signed commit could be replayed as the tip of a different meta-ref than the one its author created it for — including a signed pass replayed as the result of a different effect or commit, which is why a result’s effect and target are tree fields (<<model.result-identity>>). The creation of a hash-identified entity whose type the gate crate itself owns MUST strictly decode as that type, an unknown tree entry refusing; the gate-owned entity structs MUST stay pairwise disjoint under this decode, a property held by test rather than by a stored marker (<<meta-ref.typed-tree>>). A hash-identified entity whose type lives outside the gate’s own crate (a comment, an issue) binds by genesis oid and the all-roots walk alone (<<meta-ref.identity-binding>>): the gate cannot strictly decode a type it does not depend on, so the same signed genesis commit is not yet refused from being admitted a second time under a different such namespace (<<gate.non-kernel-strict-decode>>). When an object the binding needs is withheld by redaction, the binding MUST be vouched by the admin-signed redaction record instead of recomputed, and a redacted object MUST NOT be re-admitted (<<receive.redaction-ingest>>, <<model.redaction>>); this vouching has no implementation yet (<<gate.redaction-vouching-undefined>>).

Ownership Keys Mutation

Advancing a hash-identified entity’s ref MUST be authorized only for the member whose signature its genesis carries — ownership intrinsic to the id, which is the genesis commit’s oid — or for an admin-registered member; a review ref MUST advance only under the signature of the member its refname names (<<model.review>>). Creation stays provenance-keyed exactly as <<model.member-provenance>> routes it: a self-attested member creates and mutates through its own inbox, adopted onto the canonical ref by merge (<<gate.adoption-merge>>), which satisfies the parentless-roots walk because the contributor’s genesis remains the history’s sole root.

Identity Binding Gaps

Non-Kernel Hash-Identified Types Have No Strict-Decode Enforcement (Deferred)

<<gate.identity-binding>>'s strict-decode refusal only runs for a hash-identified entity whose Rust type the gate crate itself owns (today, a result); a comment or an issue’s type lives in a higher-layer crate the gate MUST NOT depend on, so the gate’s binding for those namespaces checks only the genesis oid and the all-roots walk, never the tree’s shape. The workspace-level pairwise-disjointness test (<<meta-ref.typed-tree>>) proves the types cannot both decode one tree; it does not by itself stop the gate from admitting the identical signed genesis commit as the tip of two different such namespaces (for example refs/meta/comments/<oid> and refs/meta/issues/<oid>), since neither admission path ever calls either type’s decoder. Closing this gap needs either a decoder a non-kernel crate can register with the gate, or moving strict decode to a layer that already depends on every entity type (receive or above); this is chosen direction, not yet designed, the same acknowledged-gap shape as <<gate.bootstrap>>.

Redaction Vouching for the Binding Has No Implementation (Deferred)

<<gate.identity-binding>> requires that when an object the binding needs has been redacted, the gate vouch by the admin-signed redaction record rather than fail trying to re-read withheld bytes. No such check exists: the gate treats a redacted (missing) object the same as any other unreadable one — for a walk like the all-roots reachability check this ends that path early rather than consulting refs/meta/redactions/*, and for a required tree-entry read it surfaces as an evaluation error the mandatory call site MUST block on (<<gate.mandatory-hosted>>), not the vouched pass-through the requirement describes. `receive.redaction-ingest’s refusal to re-admit already-redacted bytes is a different guarantee — it stops refilling a yanked object at ingest — and does not give the gate a way to keep verifying later, unrelated mutations on a ref whose history happens to reach a redacted object. Which component consults the redaction list on the gate’s behalf, and how, is chosen direction, not yet designed, the same acknowledged-gap shape as <<gate.bootstrap>>.

Fast-Forward Freshness

The new tip MUST descend from the old tip in the DAG sense: meta-refs advance fast-forward-only. The parent hash IS the anti-replay freshness binding; no nonce is needed.

Atomic Update

The ref update MUST commit via atomic compare-and-swap against the old tip read in <<gate.fast-forward>>.

Signature as Repository Data

A commit signature MUST be treated as a data artifact, not a transport artifact: it replicates with the repository and MUST verify cryptographically offline in every clone, so the signature evidence is itself repository state. Whether the signing key was authorized is a property of the snapshot the gate reads (<<gate.policy-as-state>>, <<model.member-revocation>>), not of the signature bytes. A push certificate carries no meta-ref semantics; it MUST NOT be consulted by the gate.

Policy Is Repository State

The member set and refname rules the gate consults MUST themselves live under refs/meta/*. Any frontend MUST therefore be able to evaluate the actual policy offline, with staleness bounded only by the age of its last fetch.

Verification Epoch

Verification Epoch

The gate MUST apply the tip invariant (<<gate.tip-signed>> through <<gate.atomic-cas>>) only from an epoch recorded in refs/meta/config forward; history before the epoch is archival. The epoch-setting commit MUST be the first gated tip of the config ref itself, resolving the circularity of reading the epoch from the very ref the gate verifies.

Mandatory and Advisory Enforcement

Mandatory Enforcement, Hosted

The hosted store MUST run the gate at CAS time; a failing verdict MUST abort the transaction before any ref is updated.

Advisory Enforcement, Local

The local store MUST accept any write and run the gate only as a verdict annotating the result; a failing verdict MUST NOT block the write. Enforcing the gate locally would destroy offline-first: a user could not author while unenrolled, or work against an unfetched member list. A failing local verdict, computed entirely offline against last-fetched policy (<<gate.policy-as-state>>), MUST surface the inbox alternative (<<sync.inbox-routing>>) at verdict time, not only once a push is actually rejected.

One Function, Three Call Sites

The gate MUST be the same function at every call site: hosted CAS (<<gate.mandatory-hosted>>), the local UI verdict (<<gate.advisory-local>>), and push pre-flight (<<sync.pre-flight>>). A pre-flight verdict is a prediction that can only go stale; it MUST NOT diverge from the rules the hosted store will actually apply.

Verdict Carries a Machine-Readable Reason

On failure the gate MUST return which requirement (<<gate.tip-signed>> through <<gate.atomic-cas>>) failed and the subject refname, not a bare pass/fail. Every advisory call site (<<gate.advisory-local>>, <<sync.pre-flight>>) MUST render this reason to the user — for example, "your signing key is not authorized for refs/heads/main" — so a negative verdict is actionable before a push is ever attempted. This guarantee is scoped to refs/meta/*: the enumerated requirements never fire against refs/heads/* (<<gate.principled-split>>), whose own authorization mechanism is not yet specified (<<gate.branch-acl-undefined>>), so the illustrative refname above is aspirational until that mechanism exists.

Adoption

The requirements below are consequences the gate enforces by judging only the tip plus DAG descent (this file’s own introduction: verification is a pure function over ref-store reads). The prohibition on cherry-picking as an adoption mechanism is not among them: a cherry-picked commit, signed by the authorized member placing it, satisfies <<gate.tip-signed>> exactly as a hand-authored commit would, so no function over the resulting ref state can tell the two apart. That prohibition binds the adoption tooling instead — <<sync.adoption-no-cherry-pick>>.

Adoption Is a Merge

When the author of a commit and the member authorized to place it on the canonical ref differ, adoption MUST proceed by the authorized member merging the contributor’s signed commit onto the canonical ref, even when the merge is trivial. The merge commit satisfies the tip invariant; the contributor’s signed commit remains in ancestry with attribution intact.

Direct Fast-Forward Forbidden

Fast-forwarding a canonical ref directly to a contributor’s commit MUST NOT be treated as adoption: it would place an unauthorized signature at the tip, since the contributor is not authorized for that refname.

Same-Actor Divergence

Two of a member’s own machines racing that member’s single-writer ref MUST be resolved by merging the member’s own divergent heads, not by erroring. Author and placer are both the same member, so the tip invariant holds as written once the merge tip descends from the old tip.

The Principled Split

Signatures Authorize Only Single-Writer Appends

A content signature MUST be treated as carrying write authorization only where mutations are author-signed single-writer appends, which the meta-ref granularity rule guarantees. On refs/heads/*, pushing commits the pusher did not author is legitimate, so branch refs MUST keep transport-level authorization instead of the tip invariant.

Branch-Ref Transport Authorization Is Unspecified (Deferred)

<<gate.principled-split>> requires refs/heads/* to keep transport-level authorization instead of the tip invariant, but no document in this spec defines that mechanism: what evidence it consults, where its policy lives, or how a verdict is derived from it. receive.proposal-shape’s connection-level ACL input for `refs/heads/* MUST therefore be threaded through uninterpreted — never substituted for the tip invariant on a refs/meta/* update, which remains fully specified — until a future spec addition defines this policy. No advisory call site (<<gate.advisory-local>>, <<sync.pre-flight>>) MAY render a refs/heads/* verdict in the meantime; the illustrative verdict-reason examples in this file and roots.adoc describe tip-invariant predictions against meta-refs only. This direction is chosen but not yet designed, the same acknowledged gap as <<gate.bootstrap>>.

Bootstrap

Empty Member List Bootstrap (Deferred)

When no refs/meta/member/* ref exists, the gate MUST treat the first enrollment as self-admitting so the first member can be pushed in. A member set that exists but whose keys are all revoked MUST fail closed: revoking every key MUST NOT reopen this self-admitting window. Closing "first push owns the repo" beyond this bootstrap admission is a hosted-deployment concern, deferred to <<roots.bootstrap>>.

Epoch Establishment Has No Assigned Trigger (Deferred)

<<gate.epoch>> defines what the epoch-setting commit to refs/meta/config must be, but no composition root or porcelain command is specified to produce one. A Mandatory-mode root (<<gate.mandatory-hosted>>, <<roots.single-node-hosted>>, <<roots.hosted>>) that never receives that commit runs with verification permanently off: every refs/meta/* push stays archival, indistinguishable from pre-epoch history, and the "abort before any ref is updated" guarantee never actually engages. A root establishing its own epoch as part of first boot — before any member exists to author a porcelain command — is the likely shape, mirroring this section’s own self-admitting first enrollment, but this is chosen direction, not yet designed or enforced, the same acknowledged gap as <<gate.bootstrap>>.