--- name: "tangled-https" description: "Create Tangled repositories and push commits to them entirely over HTTPS, with no SSH — using atproto service-auth JWTs against the knot's XRPC API and git-over-HTTP. Use whenever an agent needs to make, seed, or commit to a Tangled repo from an environment where outbound port 22 is blocked, or when git push fails for that reason." --- # Tangled over HTTPS only Verified end-to-end 2026-08-03 from a sandbox with **outbound port 22 blocked** (no SSH to anywhere — knots, tangled.org, or github.com). This repository was created and populated using only the steps below. ## The key fact **knot1.tangled.sh runs `knot2` (the Rust knot), not the Go `knotserver`.** This matters enormously: - Go `knotserver/git.go` has `RejectPush` → 403 "Pushes are only supported over SSH." Reading that file alone gives the wrong answer for knots that have moved on. - knot2 (`knot2/crates/knot-xrpc/src/receive.rs`) serves `git-receive-pack` over HTTP, gated by `authenticate_push`, which accepts a service JWT with lxm `sh.tangled.repo.push` as **either `Bearer` or `Basic`** — the Basic path exists so ordinary git HTTPS auth works. Check `GET /xrpc/sh.tangled.knot.version` first. knot1 was `v1.15.0`, capabilities `["knot-acl"]`. A knot still running the Go implementation will reject HTTP pushes and require SSH. knot2 also uses **different query param names** than the Go lexicons in `tangled.org/core/lexicons/` (e.g. `describeRepo?repoDid=…`, and its `MergeInput` has **no `repo` field** — passing one yields a misleading `RepoNotFound`). When in doubt read `knot2/crates/knot-xrpc/src/*.rs` rather than the Go handlers or the shared lexicon JSON. ## Auth pattern (all three calls) Mint a fresh inter-service JWT per lxm from the acting account's PDS: ``` GET /xrpc/com.atproto.server.getServiceAuth ?aud=did:web:&lxm= Authorization: Bearer ``` Audience is always `did:web:`. An app password is enough to open the session; nothing here needs the account's signing key directly. ## Recipe 1. **Create the repo.** `POST https:///xrpc/sh.tangled.repo.create` with lxm `sh.tangled.repo.create`, body `{rkey, name, defaultBranch}`. `rkey` is a TID you generate. Optional `source` = a clone URL the knot fetches server-side (server-side fork/import, no local bandwidth). Returns `{repoDid, key}` — the knot mints a `did:plc` for the repo itself. 2. **Publish the record.** `com.atproto.repo.putRecord` into the owner's repo, collection `sh.tangled.repo`, **same rkey**, record `{name, knot, repoDid, description, createdAt}`, `validate: false`. The appview ingests this via jetstream; without it the repo has no page. 3. **Push.** Get a token with lxm `sh.tangled.repo.push`, then: ```bash git -c http.extraHeader="Authorization: Bearer $TOK" \ push https:/// main ``` Success looks like `remote: received 1 ref`. Permission note: `repo:create` requires `server:member` on that knot (`rbac/rbac.go`). ## Alternative to pushing `sh.tangled.repo.merge` applies a **raw patch string** to a branch server-side (`{did, name, patch, branch, authorName, authorEmail, commitMessage, commitBody}`; `patchutil.IsFormatPatch` means `git format-patch` output is fine). Useful when there's no local clone. Push is simpler when there is one. Also available with zero repo ownership: `tg pr create` bundles a patchset into an `sh.tangled.repo.pull` record in the contributor's own repo — the fork-free contribution path, which likewise needs no push. ## Sanity checks - `git ls-remote https:///` (anonymous; HTTPS read always works) - `GET /xrpc/sh.tangled.repo.describeRepo?repoDid=…` → owner + rkey - The appview page at `https://tangled.org//`