Skip to content

Maintainer authority in ngit v3

The old model let one key speak for another

A NIP-34 repository announcement is a replaceable kind 30617 event. Its author and d identifier form one maintainer-scoped repository coordinate. In the legacy model, that event could carry a flat roster:

text
["maintainers", "<alice-pubkey>", "<bob-pubkey>"]

Alice's signature authenticated the event, but it did not authenticate Bob's membership. Treating the tag as an access-control list asked one signature to make claims for every other key in the list.

That ambiguity caused several protocol problems:

  • No consent proof. Alice listing Bob did not prove that Bob had agreed to maintain Alice's repository.
  • Authority injection. A malicious repository using the same d identifier could list a respected developer and make that developer's unrelated events appear connected to it.
  • No stable repository component. Each maintainer publishes their own coordinate. Taking the union of every list admits unilateral assignments; taking an intersection can discard valid relationships during ordinary relay delay.
  • No role semantics. The list could not distinguish a coordinating lead, a co-maintainer, or somebody permitted to moderate discussion without signing repository state.
  • No lifecycle. Replacing the list erased the distinction between an invitation, acceptance, removal, departure, and later re-invitation.
  • No safe forwarding. A client resolving an old maintainer's coordinate had no signed route to the repository's current lead.
  • Hidden consent. Some v2 operations could publish an acceptance while pushing state or performing another maintainer action. The signature did more than the command appeared to ask.

This matters especially for kind 30618 repository-state events. They do not carry enough coordinate information to disambiguate every unrelated repository that happens to share an identifier. Clients need a safe answer to a prior question: which announcement authors form the repository selected by this coordinate?

The v3 primitive: reciprocal signed edges

v3 treats each announcement as one author's view of a directed maintainer graph. A listing is an assignment or invitation. It becomes authoritative only when the subject publishes a compatible active role back into the already confirmed component.

text
Alice assigns Bob                Bob acknowledges the repository
       invitation          +             acceptance
             └─────────────┬────────────────┘

                 confirmed maintainer

The author remains part of every fact. Alice's m:Bob edge is Alice's assignment. Bob's self-m and lead relationship are Bob's acceptance. The two events are not copies of one shared roster.

This produces a useful invariant:

No pubkey joins a repository's authority component solely because somebody else named it.

An invitation remains discoverable, and its recipient can still contribute normally. Until reciprocity exists, clients ignore that recipient's kind 30618 state and maintainer-only actions for the repository.

Indexed role records

Role-aware kind 30617 announcements use indexed M, m, and o tags:

text
["M"|"m"|"o", "<subject-pubkey>", <start>, <end>, <start>, <end>, ...]
TagMeaningRepository-state authority
MLead relationship or the lead's self-roleyes, after reciprocity
mCo-maintainer assignment or self-roleyes, after reciprocity
oModerator assignment or acknowledgementno

M and m have equal cryptographic authority. The capital letter identifies the lead relationship used for coordination and forwarding; it does not make a lead's signatures stronger.

The remaining fields are alternating Unix-time start and end boundaries. A record ending in a start is active. A numeric end closes an interval, and a later start records re-entry without rewriting the earlier period:

text
["m", "<bob-pubkey>"]                         # active, start unknown
["m", "<bob-pubkey>", "100"]                # active since 100
["m", "<bob-pubkey>", "100", "200"]       # ended at 200
["m", "<bob-pubkey>", "100", "200", "300"] # active again

The literal defer may appear only as the end of a non-self record. It retains another maintainer's interval as replicated history without creating a current assignment through the copying author:

text
["m", "<bob-pubkey>", "200", "defer"]

A defer record is inactive for authority, reciprocity, and lead forwarding. A self-role ending in defer is invalid because an author must sign a real current role or a numeric departure boundary.

If an announcement contains any indexed role tag, those tags are authoritative and the legacy maintainers tag is only a compatibility projection. With no indexed or legacy role tags, the author remains the implicit sole maintainer, which keeps one-person repositories compact.

How clients resolve the graph

Resolution is relative to the selected maintainer, meaning the pubkey in the nostr:// URL, naddr, configured repository coordinate, or explicit --repo value. There is no global same-identifier roster.

For the normal lead-shaped topology, a client:

  1. Selects each author's latest kind 30617 event using NIP-01 ordering.
  2. Follows valid active M pointers from the selected coordinate until a maintainer with an active self-M identifies the terminal lead.
  3. Seeds candidates from that lead's active M, m, and o records.
  4. Confirms a candidate maintainer only when a confirmed maintainer names them and their latest announcement has a valid active M or m edge back to a confirmed maintainer.
  5. Adds assignments made by newly confirmed maintainers and repeats to a fixpoint. Historical defer records never enter this expansion.
  6. Keeps named but unconfirmed candidates as invitations. A signed self-role end vetoes stale assignments made by somebody else.

This resolver cannot be bootstrapped by a cycle of invitees. It also fails closed on missing lead targets, multiple active lead pointers, lead cycles, and other ambiguous forwarding paths.

A deliberately leadless repository uses the reciprocal active-m component rooted at the selected maintainer instead. That mode remains valid, but its result is still selected-coordinate-relative. Two groups using the same d identifier remain distinct virtual repositories until real reciprocal edges join them.

Invitation and acceptance on the wire

For a normal two-person repository, Alice begins as the implicit sole maintainer. Her first invitation materializes her lead role and assigns Bob:

text
# Alice's kind 30617
["M", "<alice-pubkey>"]
["m", "<bob-pubkey>", "T1"]

Bob accepts with his own announcement:

text
# Bob's kind 30617
["M", "<alice-pubkey>", "T2"]
["m", "<bob-pubkey>", "T2"]

At T2, the graph is reciprocal and Bob becomes confirmed. Alice can then acknowledge that evidence by changing Bob's effective start from the invitation time to the acceptance time. Other maintainers may retain the interval as an inactive defer copy instead of pretending they made the assignment:

text
T1  Alice assigns m:Bob,T1                 Bob is invited
T2  Bob signs M:Alice,T2 + m:Bob,T2        Bob is confirmed
T3  Alice records m:Bob,T2                 accepted start retained
T4  Carol copies m:Bob,T2,defer            history, not an edge

The everyday commands deliberately mirror those signatures:

bash
# Alice offers the role.
ngit repo edit --add-maintainer <bob-npub>

# Bob signs acceptance.
ngit repo accept

Lead is forwarding, not ownership

The lead and every confirmed co-maintainer can publish repository state, merge, and perform maintainer-authorized actions. The lead additionally coordinates the active roster and supplies the normal seed for graph resolution.

A co-maintainer's active M points toward the lead. The lead's active self-M terminates the walk:

text
Carol → Alice → Bob → Bob
                       ^ terminal lead

During a handover, old coordinates can preserve signed forwarding paths while maintainers converge on the new lead. This never transfers a private key or lets one maintainer replace another maintainer's announcement. A checkout that selects a different coordinate can also expose a different component while the graph is partitioned, which is why clients report incomplete or conflicting paths instead of guessing.

History authorizes historical actions

Role intervals are not merely UI audit data. They let a client ask whether the signer of an immutable issue, proposal, status, or other maintainer action held the required role when that event was published.

Replicated histories can be incomplete or disagree. The resolved view prefers the selected maintainer's record, then the shortest confirmed-graph distance, then the lowest author pubkey as a deterministic tie-break. Omission does not erase a history retained elsewhere, and conflicting signed copies remain visible even when precedence chooses one for evaluation.

Current authority is stricter: only valid active reciprocal graph edges count. An old interval, a defer copy, or a compatibility tag cannot grant present authority.

The intentional breaking change

In v2, an invited maintainer's first state push or another maintainer action could automatically publish their acceptance. In v3, the operation refuses maintainer authority and directs the invitee to run:

bash
ngit repo accept

An invitation must now be accepted explicitly before it grants authority. Consent is no longer signed as a side effect of an unrelated command.

Existing confirmed maintainers do not need to accept again. A reciprocal legacy relationship remains confirmed, including its prior signed state. A sole maintainer also continues without adding role tags.

Compatibility is a projection, not authority

Role-aware publishers emit one deprecated maintainers tag containing the subjects of every active M and m record. This lets older clients retain the best current roster they understand.

The projection deliberately cannot express reciprocity, role history, moderators, defer, or the difference between invitation and acceptance. v3 clients therefore use indexed roles whenever any M, m, or o tag exists and ignore a disagreeing compatibility list for authorization.

Legacy announcements with no indexed roles continue to use their reciprocal maintainers graph. The first v3 membership change preserves existing relationships and materializes indexed role records. An unrelated metadata edit does not silently choose a lead or fabricate timestamps.

For a multi-maintainer legacy repository, the first roster mutation requires an explicit choice between the inferred lead and a deliberately leadless model. That is a governance decision, not a migration failure.

Why synchronization matters to authority

ngit, ngit-grasp, ngit-ci, and GitWorkshop apply this graph at their respective trust boundaries. Graph rules alone are not enough if each client constructs a different repository from arbitrary relay fragments.

Proactive Sync and Sync+ help the declared repository relays converge on repository state and collaboration history. ngit and GitWorkshop can then use those relays as the authoritative collaboration view instead of restoring filtered material from unrelated fallback or account relays. That combination gives future maintainer-based moderation both a signed membership model and a well-defined place to apply policy.

Inspecting the resolved view

Run an online query once to refresh the repository view, then inspect the cached graph as JSON:

bash
ngit repo
ngit repo --offline --json

The result separates confirmed_maintainers, invited_maintainers, and lead_maintainer, and includes directional edges, pending history actions, and repository health information.

Next

Git collaboration, without the platform.