跳到正文
DEV Community· Chen Yuan·· 3 小時前AI 評分69

Tangled 如何將議題存為 Git 物件,並以 AT Protocol 重建程式碼協作平台

Issues as Git Objects: How Tangled Rebuilt the Code Forge on the AT Protocol

AI 導讀

Tangled 於 10 月 30 日推出改版,將議題、pull request、留言、反應和標籤以 Git 物件鏈儲存在程式碼庫內,並透過 AT Protocol 同步。

正文

Every issue, pull request, comment, reaction, and label on a Tangled repository is now a chain of Git objects, stored in the repository itself, right next to the code it describes. That single decision, shipped as part of Tangled's October 30 relaunch, is the most consequential forge design choice since GitHub bolted a social graph onto Git — and it inverts the usual question. The interesting thing about a federated forge is not that you can self-host it. It is that the boundary between the code and the conversation about the code has been moved into the one data structure every developer already trusts: the Git object graph.

This article walks through what Tangled actually shipped, what the new architecture is made of, and what running a piece of it yourself commits you to. Everything below comes from Tangled's relaunch post, its self-hosting documentation, and the AT Protocol sync specification, not from launch-day enthusiasm.

The centralized forge bargain is ending

For most of the past two decades, the code forge has been where projects live: where code is hosted, where changes are reviewed, and where contributors find each other. The bargain was convenience in exchange for centralization, and Tangled's own relaunch post is blunt about how that bargain aged. Relying on someone else for uptime, for features, and to be a good steward of your code has not played out well.

The post draws a timeline worth taking seriously. In 2005 the stack was mail plus Git. In 2009 it became GitHub plus Git. Now, Tangled argues, it becomes AT Protocol plus Git. What survived both previous eras was not a company but a pair of open protocols: Git to move the code, and an open communication layer to talk about it. Email was that layer in the first era, and it worked, at the cost of user experience. The pitch for Tangled is that the AT Protocol can be that layer without giving up the polish of a proper forge.

The federation claim is specific rather than rhetorical. You can self-host just the parts you want, like Git hosting or CI runners, or all of it, including the API server and the UI. Whatever you choose, you can still talk to everyone else, because the collaboration data does not belong to whichever server happens to render it.

From one Go monolith in Sweden to the edge

Until this release, Tangled's web UI and backend were a single Go monolith called the appview. It handled AT Protocol indexing, caching, off-protocol data, and UI rendering with html/template and htmx, and it ran on a single SQLite database on one medium-sized server in Sweden. That worked well as long as you were roughly 50 to 100 milliseconds away from Sweden, which most of Tangled's users are not.

The replacement splits the monolith along a clean line. A new standalone frontend written in Svelte is deployed at the edge. Behind it sits bobbin, a new indexer service written in Rust that is diskless and entirely in-memory, deployed across more than eleven regions so that API and UI latency stops being a function of distance from Scandinavia. Bobbin is purely read-only and deals only with AT Protocol data. Everything else, the off-protocol state a forge still needs, has been delegated to a new set of supporting services.

Alongside the split, the data model moved. A new set of lexicons now lives under org.tangled., replacing the old sh.tangled. records. If you consumed the old records from the firehose, that namespace change is a breaking one, and it is worth grepping your consumers for before assuming silence means inactivity.

Knots now behave like a PDS

The deepest change is knot2, the second implementation of Tangled's knot server, this time written in Rust. A knot is the component that stores Git repositories. Knot2 bakes in a fully custom Git server and bundles an SSH server into the same binary, so the process that answers your git push is the same process that speaks to the wider network.

What makes knot2 interesting is where it puts the collaboration data. Issues, pull requests, comments, reactions, and labels are now stored on the knot, right next to the code, as collaborative objects, COBs for short. A COB is just a chain of Git objects in the repo itself. Collaborators, knot members, and the blocklist are stored the same way.

Conceptually, an issue thread is no longer a row in someone else's database. It is a chain of objects whose history is Git history:

issue object          comment object          label object
  "opened"      <-      "reply"          <-      "add: bug"
      |                      |                        |
      +---------- signed, ordered, replicated --------+

In that sense, knots now behave a lot like a PDS. They hold signed records, full history included, for everything that belongs to a repo, and much like a PDS, those records are synced via the standard AT Protocol firehose.

Why Git objects are a real design decision, not a gimmick

Storing collaboration in Git objects buys properties that centralized forges simulate with export buttons. Portability is the obvious one: if the discussion lives in the repository, then cloning the repository carries the discussion with it, and the forge that hosts the canonical copy becomes a preference rather than a prison. Authenticity is the second: AT Protocol records are signed, and the sync specification is explicit about what that buys — redistribution of public data between independent participants that is trustworthy, low-latency, and resource-efficient at large scale.

The sync layer is worth understanding because none of it is Tangled-specific. The protocol offers two mechanisms. Batch transfer uses full-repository exports as CAR files over HTTP through the com.atproto.sync.getRepo endpoint, which is unauthenticated and returns all repo records, the Merkle search tree nodes, and the current signed commit object in a single file. Real-time sync is a repository event stream over WebSocket, the firehose, where every commit carries a revision value encoded as a TID that functions as a logical clock. A minimal consumer pointed at a relay that carries the Tangled collections looks like this in skeleton form:

import asyncio

import websockets  # pip install websockets

RELAY = "wss://relay.example.com/xrpc/com.atproto.sync.subscribeRepos"

async def main() -> None:
    async with websockets.connect(RELAY) as stream:
        async for frame in stream:
            # Frames are CBOR: a header and a payload. A real consumer
            # decodes both with a dag-cbor library, keeps the per-repo
            # revision (the TID) as a resumable cursor, and filters
            # commit ops by collection, here anything under org.tangled.*
            print("frame bytes:", len(frame))

asyncio.run(main())

The discipline that matters in a real consumer is the cursor. The specification requires revisions to increase between commits for the same repository, even across host migrations, and synchronizing services should reject updates whose revisions sit in the future beyond a short drift window. That gives you exactly what an indexer wants: a resumable, ordered stream with a cheap freshness check.

Deletion deserves an honest mention. The sync specification obliges mirrors to respect record deletion and account deactivation in a timely manner, seconds or minutes rather than days. When the conversation lives in a replicated object graph, delete is a protocol-level request that every downstream consumer has to honor. That is a stronger guarantee than a row update inside one company's database and a weaker one than true erasure, and your own consumers should be designed with that gap in mind.

Run your own knot in an evening

The self-hosting documentation is concrete about what a knot needs: a Linux machine that stays online, a DNS name you intend to keep because the knot uses it as its identity, your DID (look up your handle at pds.ls if you do not know it), and a reverse proxy such as Caddy for TLS. The master key is a random 32-byte base64 value kept in an env file with mode 600.

The Docker path is a single image, atcr.io/tangled.org/knot:2, plus a config.toml:

[server]
hostname = "knot.example.com"
admins = ["did:plc:yourdid"]
listen_addr = "[::]:5555"
ssh_host_key_file = "/data/ssh/host_key"

[repo]
scan_path = "/data/repos"

[secrets]
sealed_key_file = "/data/secrets/sealed-keys"
master_key_env = "KNOT_MASTER_KEY"

[atproto]
plc_directory = "https://plc.directory"

Compose maps two ports, 2222 for SSH where your users clone and 127.0.0.1:5555 for the HTTP side behind your proxy, and three volumes for repos, secrets, and the SSH host key. Bringing it up and checking it is a four-line operation:

mkdir -p repos secrets ssh
sudo chown -R 1000:1000 repos secrets ssh
docker compose up -d
curl https://knot.example.com/xrpc/_health

On NixOS there is a flake module instead: add the Tangled flake input, enable services.tangled.knot-rs, point it at the same env file, and the knot listens on 127.0.0.1:5555 either way. Once the health check answers, you register the knot in the web app under the knots settings page, and you can create repositories on it immediately.

If you want to go deeper, the index tier is self-hostable too. Bobbin keeps everything in memory and rebuilds from a hydrant stream on every start, so it needs no database. It does need a slingshot for records and identities, a service DID of its own such as a did:web for your index host, and a knot mirror — without the mirror URL configured, repository pages have no code. The binary prints every setting with defaults via bobbin config-template and checks your file via bobbin validate before you start it.

Organizations, stacked PRs, and what ships next

Two product changes round out the relaunch. Organization accounts are real AT Protocol accounts: each has its own DID and handle, hosted on a Tranquil PDS instance. They are passwordless, delegated accounts. You create one from the New Organization page, you get a name under tngl.sh with yourself as owner, and owners can invite members.

The pull request flow now supports stacked PRs for everyone, not only Jujutsu users. If you use vanilla Git, you can interdiff between versions of a pull request, what the UI used to call rounds, and JJ users can interdiff between individual changes as well. For anyone who has watched stacked-diff workflows become the differentiating feature of paid review tools, seeing interdiff land in a federated forge is a signal about where the baseline is heading.

The launch is staggered across the coming weeks: private repositories, a CLI, SDKs for different languages, and skills for your agent are all announced but not yet shipped. The extension points are already real, though. Because the whole stack sits on the AT Protocol, you can write a custom knot — the relaunch post links one that runs on Cloudflare Workers and uses Pierre's code.storage for Git storage — or a custom spindle for CI, or host the ultralight API server and build an entirely custom forge on top. The bobbin API is explicitly documented for agent consumption, which is a quiet admission about who the next generation of forge users will be.

The contract you actually sign

A federated forge does not remove trust; it relocates it. You still depend on relays and indexers for discovery, on the PLC directory for identity resolution, and on the discipline of every mirror to honor deletions. What changes is the exit cost. When your issues, reviews, and labels are Git objects on a knot you control, and the social identity is a DID rather than a row in a user table, moving your project stops being an export-and-pray operation and becomes a replication target change.

The pragmatic adoption path is mirror-first: run a knot, put a real project on it, keep your existing forge as a read mirror, and learn the operational shape of COBs and firehose cursors before you bet a community on them. The protocol underneath has already proven it can carry a social network at scale, and watching it carry code review will be the more interesting test. Either way, the era in which the forge owns the conversation about your code just got its first credible challenger.


Originally published on Dispatch.

來源:DEV Community · dev.to