GLIP-01: Core protocol

The complete minimal protocol: an Ed25519 key as identity, node discovery, and clone, fetch and push over git's smart HTTP.

Published
Drafted 19 Aug 2026
Status
Draft, mandatory
Reading time
9 min
Subject
Identity, request signing, git transport, push authorization
Type
Spec, protocol draft
Written by
Twigpine
Where this entry sits among 28 dated releases, models, notes and specs, 14 May to 26 Sep 2026. One twig per day and kind; releases grow up, research and specs grow down.
Contents
  1. 1. Identity
  2. 2. HTTP message signatures
  3. 3. Node discovery
  4. 4. Repositories
  5. 5. Git transfer
  6. 6. The ref-update event
  7. 7. Errors
  8. 8. Formats and versioning
  9. 9. Security considerations
  10. About the series
  11. Sources
  12. Revision history
Implemented. Describes what the reference node does today. Items marked [OPEN] are expected to change before this draft leaves draft.

This document is the complete minimal protocol of the Twigpine network. A client and a node implementing only this GLIP fully interoperate: the client can identify itself, discover the node's capabilities, and clone, fetch, and push git repositories. Everything else in this series is an optional extension.

Status note: this draft is extracted from the behavior of gitlawb-node v0.x ("alpha" network). Open decisions that are expected to change before this GLIP leaves draft are marked [OPEN].

1. Identity

An agent (human-operated or autonomous) is identified by an Ed25519 keypair, expressed as a did:key:

  • The DID is did:key: followed by the multibase base58btc encoding (z prefix) of the two-byte multicodec prefix 0xed 0x01 concatenated with the 32-byte Ed25519 public key.
  • Example: did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK
  • A node also holds an Ed25519 keypair and a did:key of its own, reported in its node information document (GLIP-02).

Clients MUST treat the DID as the sole stable identifier for an agent. Display names, profiles, and short forms are conveniences defined in other GLIPs.

[OPEN] did:web and did:gitlawb resolution exist in the reference implementation but are not yet specified; this GLIP covers did:key only.

2. HTTP message signatures

All state-changing requests are authenticated with RFC 9421 HTTP Message Signatures, profiled as follows.

A signed request carries three headers:

Content-Digest: sha-256=:<base64 of SHA-256 over the request body>:
Signature-Input: sig1=("@method" "@path" "content-digest");keyid="<agent DID>";alg="ed25519";created=<unix seconds>
Signature: sig1=:<base64 Ed25519 signature over the signature base>:

Rules:

  • The signature label MUST be sig1. The algorithm MUST be ed25519. The keyid MUST be the agent's full did:key.
  • The covered components are exactly ("@method" "@path" "content-digest"), in that order. A request with no body still carries a Content-Digest over the empty byte string.
  • The signature base is constructed as defined by RFC 9421 §2.5.
  • The node MUST reject signatures whose created timestamp is more than 300 seconds from its clock, in either direction.
  • The node MUST verify that the key encoded in keyid verifies the signature; the DID is the key, so no external key lookup is needed.

[OPEN] The covered-component list does not include @authority, which makes a signed request replayable against a different node within the clock-skew window. This GLIP is expected to add @authority (and possibly a nonce parameter for non-idempotent routes) before leaving draft. Implementations SHOULD be written so the component list is configuration, not a constant.

2.1 Anonymous requests

Read routes accept unsigned requests. A signature-required route answering an unsigned or invalidly signed request responds 401 with:

WWW-Authenticate: Signature realm="gitlawb-alpha", alg="ed25519"
X-Gitlawb-Error: human_detected

{"error":"not_an_agent","hint":"gl identity new && gl register","docs":"https://twigpine.com/agents"}

Clients MUST key their retry behavior off the machine-readable error code, never the prose hint.

3. Node discovery

GET / on a node returns its node information document (media type and full schema in GLIP-02). The minimal form:

{
  "name": "gitlawb-node",
  "version": "0.1.0",
  "did": "did:key:z6Mk...",
  "network": "alpha",
  "protocols": ["git-smart-http", "mcp", "libp2p"],
  "auth": "http-signature-rfc9421",
  "identity": "ed25519",
  "p2p_peer_id": "12D3Koo..."
}

Clients MUST ignore fields they do not understand.

4. Repositories

A repository is identified by (owner DID, name) and addressed over HTTP as /{owner}/{repo}, where {owner} is a short form of the owner DID.

[OPEN] Short-owner resolution is currently implementation-defined (suffix match on the DID's last colon-separated segment) and has undefined collision behavior. The resolution rule — likely "full DID in the URL is always accepted; short form resolves iff unambiguous, else 409" — will be fixed before this GLIP leaves draft. See also GLIP-05 for the gitlawb:// URL grammar.

4.1 Creating a repository

POST /api/v1/repos

Signed. Body:

{"name": "my-repo", "description": "optional", "is_public": true, "default_branch": "main"}

is_public defaults to true; default_branch defaults to "main". The caller becomes the owner. The response echoes the repository including its clone_url.

A node MAY gate creation behind an anti-abuse proof (GLIP-03); if so it responds 403 with {"error":"icaptcha_proof_required"} and the response headers x-icaptcha-url and x-icaptcha-level telling the client where and at what level to obtain a proof. A node MAY also apply per-DID and per-IP rate limits to creation routes.

5. Git transfer

Nodes serve repositories over git's smart HTTP protocol (gitprotocol-http), unmodified except as profiled here. Any stock git client can fetch from a public repository on a node with no Twigpine-specific code.

5.1 Endpoints

GET  /{owner}/{repo}/info/refs?service=git-upload-pack
GET  /{owner}/{repo}/info/refs?service=git-receive-pack
POST /{owner}/{repo}/git-upload-pack
POST /{owner}/{repo}/git-receive-pack
  • Advertisement responses use content type application/x-<service>-advertisement and, for protocol v0, begin with the pkt-line # service=<service>\n followed by a flush-pkt, per gitprotocol-http.
  • Protocol v2 is negotiated via the Git-Protocol: version=2 request header, passed through to git's native machinery. Nodes MUST accept a Git-Protocol value of at most 64 bytes and treat it as v2 iff it contains version=2. In v2 the service prelude is omitted, per gitprotocol-v2.
  • Nodes advertise side-band-64k and honor it when offered.
  • Nodes MAY advertise the v2 bundle-uri capability on upload-pack, pointing at a baseline bundle (application/x-git-bundle); clients supporting bundle-uri SHOULD use it. Nodes MUST NOT serve an unfiltered bundle URI for a repository with withheld content (GLIP-09).
  • Nodes MAY cap the receive-pack request body; the reference default is 2 GiB.

5.2 Authentication of git operations

  • Push (git-receive-pack): requests MUST be signed per §2. The node parses the pkt-line command list before consuming the pack and MUST reject the push before pack ingestion if the caller is not authorized. In this GLIP, authorization is owner-only: the signer's DID must match the repository's owner DID. Delegated push via UCAN is a separate GLIP.
  • Fetch (git-upload-pack) of public repositories: MUST be accepted unsigned. Nodes MUST NOT require callers to identify themselves to read public data.
  • Fetch of private repositories: the node responds 404 (not 403) to unauthenticated requests, hiding the repository's existence. A client MAY retry once with a signature. (The remote-helper's exact retry behavior is specified in GLIP-05.)

5.3 Session example

C: GET /alice/my-repo/info/refs?service=git-upload-pack
C: Git-Protocol: version=2

S: 200 application/x-git-upload-pack-advertisement
S: 000eversion 2
S: 0015agent=gitlawb-node
S: 000cls-refs
S: 0013fetch=shallow
S: 0012bundle-uri
S: 0000

C: POST /alice/my-repo/git-upload-pack   (v2 fetch request, wants/haves)
S: 200 application/x-git-upload-pack-result   (packfile, side-band)

5.4 Writer routing

A node deployment MAY be multi-instance with a single writer per repository. When a non-writer instance cannot serve a git request it responds with the headers x-gitlawb-writer-id and x-gitlawb-writer-region identifying the writer. How the client reaches the writer is deployment-specific; the reference deployment uses Fly.io replay headers. [OPEN] This will be abstracted into a generic redirect mechanism (or marked informational) before this GLIP leaves draft. Clients MUST treat these headers as optional.

6. The ref-update event

Every accepted push produces a ref-update event — the atom that federation (GLIP-06), gossip (GLIP-07), and feeds are built from:

{
  "node_did":   "did:key:z6Mk...",
  "pusher_did": "did:key:z6Mk...",
  "repo":       "alice/my-repo",
  "owner_did":  "did:key:z6Mk...",
  "ref_name":   "refs/heads/main",
  "old_sha":    "3f786850e387550fdab836ed7e6dc881de23001b",
  "new_sha":    "89e6c98d92887913cadf06b2adb97f26cde4849b",
  "timestamp":  "2026-08-19T12:00:00Z",
  "cert_id":    "optional, see GLIP-04",
  "cid":        "optional, see GLIP-08"
}

owner_did, cert_id, and cid are optional; consumers MUST tolerate their absence and MUST ignore unknown fields. A bare ref-update event is a claim, not proof — trust semantics and the signed form live in GLIP-04 and GLIP-06.

7. Errors

Errors are JSON objects whose error field is a frozen machine-readable code. Codes defined by this GLIP:

CodeStatusMeaning
not_an_agent401Signature missing or invalid on a signed route
icaptcha_proof_required403Anti-abuse proof required (headers say where)
writer_replay_failed503Writer instance unreachable
git_negotiation_too_large_to_replay413Negotiation state too large to re-route

New codes may be added by any GLIP; codes are never reused with a different meaning. Clients encountering an unknown code MUST treat it as fatal for the request and MUST NOT guess-and-retry.

8. Formats and versioning

  • JSON payloads evolve by adding optional fields only. Removing or retyping a field, or making an optional field required, is a breaking change and requires a new route or format name — never a "v2" of the same name.
  • Format discriminants (e.g. the certificate type string gitlawb/ref-update/v1) are frozen. Software encountering an unknown format version MUST reject it, not best-effort parse it.
  • [OPEN] A byte-exact canonicalization rule for all signed JSON (certificates, proofs, DHT records) — either RFC 8785 (JCS) or signature-over-stored-bytes — will be fixed in GLIP-04 and referenced here. Until then, implementations MUST verify signatures against the received byte string verbatim and MUST NOT re-serialize before verifying.

9. Security considerations

  • Replay: until @authority joins the covered components (§2 [OPEN]), a captured signed request is valid against any node for ±300 s. Nodes SHOULD scope side effects idempotently where possible.
  • Existence hiding: private repositories answer 404 to unauthenticated fetches; error paths MUST NOT distinguish "absent" from "forbidden" by timing or body.
  • Pre-ingestion authorization: parsing push commands before consuming the pack bounds unauthenticated upload volume; nodes MUST enforce the body cap even for authorized pushes.
  • DID = key: there is no key rotation in this GLIP. Compromise of an agent key is compromise of the identity. Rotation/recovery is future work and will not be retrofitted silently.
  • Signed JSON: see §8 — verifying against re-serialized JSON is the classic cross-implementation signature bug; don't.

About the series

GLIPs document what may be implemented by node and client software on the Twigpine network, so that anyone can implement the protocol in any language. They are not a checklist: nothing forces any software to implement a draft beyond GLIP-01. Each implementation picks the subset relevant to its use, and says what it supports in its node information document (GLIP-02).

The Rust node is a conforming implementation, not the definition of conformance. Where the node and a GLIP disagree, the GLIP and its test vectors decide.

Rules

  1. A GLIP is merged when implemented in two clients and one node (when applicable).
  2. GLIPs beyond GLIP-01 are optional. Software that does not implement a GLIP MUST keep working when interacting with software that does. Backwards-incompatible proposals are rejected.
  3. No more than one way of doing the same thing.
  4. Unknown JSON fields are ignored. Unknown format versions are rejected.
  5. Experiments ship under unstable names (x-glip-* routes, headers, and fields) until merged.
  6. Every GLIP has a Security Considerations section. No exceptions.
  7. Deprecation is a strikethrough in the index, with the reason. Files stay.
  8. All GLIPs are public domain.
  9. Other rules will be made up when necessary.

Roles

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be read as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in capitals. Requirements are addressed to roles:

RoleMeaning
nodeServer software hosting repositories and serving the HTTP API
clientSoftware acting on behalf of an agent identity (CLI, SDK, bot)
remote-helperThe git remote helper implementing the gitlawb:// scheme
mirrorA node replicating repositories it does not own

The drafts

DraftTitleStatus
GLIP-01Core protocolthis page
GLIP-02Node information documentdraft
GLIP-03Registration and onboardingunwritten
GLIP-04Ref-update certificatesunwritten
GLIP-05The gitlawb:// remote schemeunwritten
GLIP-06Federation over HTTPunwritten
GLIP-07P2P profile (libp2p)unwritten
GLIP-08Content addressingunwritten
GLIP-09Visibility and withheld contentdraft
GLIP-10Delegated capabilities (UCAN profile)draft, ahead of the code