From 5317c27232cf6bdb51afb26b1e773d88daf466a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 20:15:42 +0000 Subject: [PATCH] Add SKILL.md with the HTTPS-only recipe; correct readme --- SKILL.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 11 ++++++-- 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..2158630 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,84 @@ +--- +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//` diff --git a/readme.md b/readme.md index 5d69a6b..c41dc09 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,16 @@ # https-only-test This repository was created and populated entirely over HTTPS, from a sandbox -with outbound port 22 blocked. No `git push` was ever run. +with outbound port 22 blocked. No SSH was used at any point. - `sh.tangled.repo.create` (knot XRPC) created the bare repo - `com.atproto.repo.putRecord` published the `sh.tangled.repo` record -- `sh.tangled.repo.merge` (knot XRPC) applied this commit as a patch +- `git push` over **HTTPS** landed the commits, authorized by a service JWT + with lxm `sh.tangled.repo.push` -Auth for both knot calls was an atproto inter-service JWT minted via +Auth for the knot calls was an atproto inter-service JWT minted via `com.atproto.server.getServiceAuth`, audience `did:web:knot1.tangled.sh`. + +See [`SKILL.md`](./SKILL.md) for the full recipe and the gotchas found along +the way — chiefly that knot1 runs the Rust `knot2`, whose HTTP push support +differs from the older Go `knotserver`. -- 2.51.2