Repository navigation
Write a language-agnostic protocol specification (PROTOCOL.md) #367
Description
Summary
There's no language-agnostic protocol specification, so a node can only be implemented by reading the Rust source. To let anyone build an interoperable node (or client) in another language, we need a PROTOCOL.md (or docs/PROTOCOL.md) that documents the wire contract independently of the reference implementation.
Why
"Anyone can run a node" is only true if the protocol is specified separately from gitlawb-node. Today the auth scheme, DID methods, ref-update certificate schema, HTTP API shapes, and IPFS/IPNS mapping all live implicitly in the Rust crates. A spec:
- lets a second implementation (Go, TypeScript, Python…) interoperate with existing nodes;
- pins down what is protocol (must match across implementations) vs. implementation detail (free to differ);
- gives reviewers and integrators a single source of truth.
Proposed scope
A first PROTOCOL.md covering, sourced from the current code:
- Identity & DIDs —
did:key/did:web/did:gitlawb; key type (Ed25519); DID → verifying-key resolution. - Authentication — RFC 9421 HTTP Signatures. The exact
Signature-Inputcovered components (@method,@path,content-digest),keyid= signer DID,alg="ed25519",created, andContent-Digestconstruction. Which routes require signatures. - iCaptcha (proof-of-intelligence) — the
403 icaptcha_proof_requiredflow,x-icaptcha-url/x-icaptcha-level/x-icaptcha-proofheaders, which writes are gated. - Ref-update certificate — the frozen v1 schema (
gitlawb/ref-update/v1): body fields, canonical signing bytes, signature entries, threshold/countersignature semantics. - Attestations — the external-provenance attestation format bound to a cert hash, and verification policy (
RequireAllleniency). - HTTP API surface — the
/api/v1/*resources (repos, refs, certs, agents, tasks, bounties, peers, resolve, stats) with request/response shapes and status/error conventions (incl. 404-shaped denials). - Git transport — smart-HTTP endpoints (
/{owner}/{repo}/info/refs,git-upload-pack) and thegitlawb://remote-helper URL scheme. - Content addressing / storage — git SHA-256 → IPFS CID mapping, branch refs as IPNS records,
/ipfs/{cid}retrieval. (Which parts are normative vs. optional.) - P2P — libp2p peer announce/gossip/sync message shapes and signed-peer enforcement (as far as they're part of the wire contract).
- Versioning — schema version discriminants and how nodes reject unknown versions.
Each section should mark normative (required for interop) vs. informational (reference-implementation behavior).
Approach
- Start as an outline/skeleton, fill section by section from the code, keep PRs reviewable (one section or a few per PR rather than one giant drop).
- Flag anything that turns out to be underspecified or Rust-specific as we go — those are the interop-blocking gaps worth surfacing.
I've been in the node internals recently (storage docs #363, the attest/core verification fixes #365/#366) and am happy to draft the first pass. Opening this to agree on scope and structure before writing — in particular: preferred location (PROTOCOL.md vs docs/PROTOCOL.md), and whether you want a formal normative/informational split from the start.
Activity
beardthelion commentedon Sep 9, 2026
@andreolf you asked here for scope and structure before writing, and nobody answered. That is on me,
and it went worse than a slow reply: #372 opened doing this work, and what discussion there was
happened there instead of here, so you had no way to see it from this issue.
Where it actually stands, stated as it is rather than as a decision: kevincodex1 raised the
possibility on #372 of putting the protocol and specs in a separate repo to keep them clean, and said
he would ping when it is ready. Nothing has been created yet, and there is no decision recorded
anywhere else. So the location question you asked is still genuinely open, not settled behind your
back.
On the normative/informational split: yes, and the code argues for a third category you did not ask
for. There are places where the implementation is internally consistent, documented, and deliberately
NOT what the relevant RFC says. Those need marking as "true of this implementation, non-standard, do
not follow the RFC here", because an implementer who trusts the RFC over the spec builds something
that fails in a way the spec would otherwise be blamed for. The clearest one:
@pathin our RFC 9421signing string carries the query, bound from
path_and_query(), where 9421 defines@pathaspath-only with
@queryseparate. It bites immediately, because?service=is mandatory on the onesigned GET in the git transport.
Where a wrong sentence actually causes silent failure
I had assumed sections 2 and 4 were the hazard. Section 2 is. Section 4 is not, and the reason is
worth knowing before you spend time on it.
The real landmine is canonicalization, and it cuts across both. There are three different schemes in
the tree and two of them are labelled canonical:
RefUpdateBody::to_signing_bytesis documented "canonical JSON bytes for signing" but isserde_json::to_vecover a struct, so it is declaration order.gitlawb-attest::cert_hashis genuine JCS (RFC 8785).gitlawb-node's ownissue_ref_certificatesigns aserde_json::json!Value, and with nopreserve_orderfeature that Map is a BTreeMap, so those bytes are lexicographic.serde_jcsappears only ingitlawb-attest. So an implementer who reads that crate's excellent "JCSso they reproduce across implementations" line and applies it to the certificate's own signature
produces bytes that never verify, with no diagnostic beyond a signature failure. If the spec says one
sentence about canonicalization it has to say which of the three applies where.
Section 4 is a different problem: the ref-update certificate has no wire presence at all.
RefUpdateCertappears in no node, client, or remote-helper code;gitlawb-attestis a workspacemember nothing depends on. It is also unconstructible as written, requiring 64-hex object ids while
the node creates repos with
--object-format=sha1andparse_ref_updateshard-codes 40. I wouldmark it planned rather than specify it, and say so explicitly, since a spec that documents an unused
type as protocol is worse than one that omits it.
The thing in that area that IS load-bearing and unwritten: a SHA-256 repo's push parses to zero ref
updates because of that same 40-char assumption, so branch protection, certificates, webhooks and
replication all silently no-op while the push itself succeeds.
Section 8, and a correction to your framing
Your instinct to split normative from informational matters most here, and the honest version is
blunter than yours: essentially all of it is optional, and two of its three claims do not describe
shipped behavior. IPNS is not implemented, one comment mentions it; branch tracking is a SQL upsert,
unsigned, last-write-wins. And the git-to-CID mapping is not a SHA-256 correspondence: the CID digests
the unframed payload while git digests the framed object, so they are different by construction, and
the reverse lookup goes through a table rather than arithmetic.
/ipfs/{cid}is not a network read atall; it serves from the local git object store.
What your ten sections miss
This is the part worth your time, and it is why the scope conversation was worth having before
writing rather than after. In rough order of how badly a second implementation breaks without it:
X-Ucanis read and fully validated on every signed request, and its encoding is not thestandard UCAN JWT: it is
{payload, s}signed over declaration-order JSON. Someone who knows UCANbuilds a JWT and gets 401. It currently grants no authority, which is itself a thing to state.
POST /graphqlplus/graphql/wssubscriptions, withits own queries, mutations and subscriptions, mounted outside the auth layer for the socket.
clone of such a repo yields a reachability-incomplete pack that a stock full clone rejects. There
is a
withheld-pathsendpoint that exists precisely so a client can sparse-exclude. Normative forany second client, absent from your list.
{owner}is a DID, not a username, and the path segmentis the bare multibase key with
did:key:stripped, case-sensitively. Nothing in the scope says howa client turns a DID into a URL.
retry-after,503 with
Retry-Afterfor capacity shedding, 504 for git timeouts, and a 413 whose body is plaintext rather than the JSON envelope every other error uses.
WWW-Authenticate: Signature realm="gitlawb-alpha", alg="ed25519"onunauthenticated push. There is no Basic challenge, so git credential helpers are inert.
?service=mandatory with no dumb-HTTPfallback, protocol v0 only with
GIT_PROTOCOLnever propagated, and no gzip request decompressionanywhere, so a gzipped request from stock git is a 400.
X-Gitlawb-Signature-256, not Ed25519, not 9421.GLENCmagic, version 2, XChaCha20-Poly1305 with per-recipientX25519-from-Ed25519 key wrapping, served over its own routes.
owner-push enforcement, signed-peer writes, and the iCaptcha mode, which defaults to off.
formats in the tree.
Two smaller corrections while you are in here. Identity:
did:webanddid:gitlawbparse andvalidate, but only
did:keyresolves to a verifying key, and anything else is a 400 at auth, so thespec should scope identity to
did:keyand say the others are reserved. iCaptcha: the node reads onlyx-icaptcha-prooffrom a request;x-icaptcha-urlandx-icaptcha-levelare response-only.The model to copy
crates/gitlawb-attestis already the thing you are proposing, for one narrow surface, and it isgood: wire shape, JCS canonical bytes, domain-separated signing input, and a canonical-vectors test
whose own docstring says it exists so a port to another language has an oracle to check against
without reading Rust. Whatever shape the spec takes, that plus vectors is the bar. A spec section
without a test vector is a section that will drift.
What #372 empirically taught
Worth knowing since it is now evidence rather than prediction. The first review round was ordinary
factual accuracy. The second was the harder class: statements individually true that still lead an
implementer to build something broken. That is the argument for your split, plus the third category
above.
If you still want to drive this, say so here and I will make sure you are looped in wherever it lands
rather than finding out through a PR again. Your storage docs in #363 and the verification work in
#365 and #366 are the right background for it, and the canonicalization question above is the one I
would most want your eyes on regardless of who writes the document.