--- id: api title: Everything the lexicon has, reachable without a verb for it status: shipped repos: [atgc] dependsOn: [sign-in] exitCriterion: > Any XRPC method a PDS or a Tangled service serves can be called with the right credential, without atgc growing a verb and without a new scope. --- # api Tangled's lexicon is far larger than the part atgc has verbs for — labels, stars, follows, collaborators, notifications — and none of it was reachable at all. `atgc api ` is the hatch. The design question was which credential, and the answer is that the host decides: `--host pds|knot:|appview|bobbin`. "An XRPC call" is three unrelated acts here — a PDS call under the session's DPoP-bound access token, a knot procedure under a service-auth JWT minted for that one method, a public read under nothing — so a flag naming the credential would ask the user to already know the thing they came here to find out, and a wrong guess would hand a third-party knot a token for the PDS. It adds no scope, and could not: a scope added to `SCOPES` is a re-login for every account, and an escape hatch is the last thing that should cost one. ## What it needs - [ ] `atgc api` cannot address a service by URL, only by the four names `--host` takes, so reading *another* account's PDS directly — the shape that answers "does their record really say that" — is not reachable and `--host pds` always means the acting account's own. The names are what make the credential derivable; a URL would have to be sent unauthenticated, or be asked which credential it takes, and both are worse than not having it. `ATGC_BOBBIN`, `ATGC_APPVIEW` and `ATGC_KNOT` still move the three that are not the PDS - [ ] `atgc api --host knot:` needs the knot's hostname typed out. Deriving it from the checkout means turning a repo DID back into an owner and a name, which only the appview's index can do — the same gap `doctor`'s knot row has — and guessing it off the `origin` remote is wrong for every repo cloned through tangled.org, which proxies git for its knots: the guess would mint a service-auth token for `did:web:tangled.org` and send it to something that is not the knot. Refusing to guess is the right failure, but it does mean `atgc repo view` first - [ ] `atgc api` does not write the DPoP nonce back to the session store. `DpopCall` retries once on a nonce challenge by itself, so the cost is one extra round trip on the first `api` call of a process; carrying it back would mean this command rewriting jacquard's own session record behind jacquard's back, which is a worse trade for one round trip - [ ] `atgc api` sends JSON bodies only, so `com.atproto.repo.uploadBlob` — the one PDS procedure whose body is bytes — is not reachable through it, and neither is `gh api`'s `-F key=@file`. `--input` is the file-shaped answer here and two spellings of it would be a choice with no right answer, but a blob upload genuinely has no spelling yet ## Done - [x] `api` — an authenticated raw XRPC escape hatch, `atgc api `. Tangled's lexicon is far larger than the part atgc has verbs for — issues, labels, stars, follows, collaborators, secrets, artifacts, pipelines, notifications — and none of it was reachable at all. The design question was which credential, and the answer is that the host decides: `--host pds|knot:|appview|bobbin`. "An XRPC call" is three unrelated acts here — a PDS call under the session's DPoP-bound access token, a knot procedure under a service-auth JWT minted for that one method, a public read under nothing — so a flag naming the *credential* would ask the user to already know the thing they came here to find out, and a wrong guess would hand a third-party knot a token for the PDS. A knot's queries and its procedures therefore differ under one `--host`, which is not a special case: its queries are public and its procedures are not - [x] `api`'s parameters are `gh api`'s, spelling and meaning both, because that is the muscle memory people arrive with: `-F` guesses a type, `-f` never does, and both go in the query string of a query and the JSON body of a procedure. XRPC declares query-or-procedure and a method name does not, so `--input` means a procedure, `-X` settles it, and a wrong guess comes back as the 404/405 it really is with a line naming the other verb. No `--json`: the output is the service's own answer, so the flag would be a switch with nothing behind it. A refusal prints the service's `error` and `message` and exits as that kind of refusal — the XRPC error name decides over the HTTP status, since a PDS answers `RecordNotFound` with a 400 - [x] `api` adds no OAuth scope, and could not: a scope added to `SCOPES` is a re-login for every account, and an escape hatch is the last thing that should cost one. What it reaches is what the grant already covers, and a `createRecord`/`putRecord`/`deleteRecord`/`applyWrites` whose collection the grant does not name fails with `scope.rs`'s own refusal before the request, naming the scope and the login that would grant it. A knot procedure goes through `require_rpc` the same way. Writes made this way land in `pds.jsonl` like every other write, because the DPoP arm sends through `logging::pds::LoggedPdsClient` — a hatch able to create a record outside that log would be the one hole in it - [x] `atgc api` reads no more of an answer than anything else does: `http::MAX_BODY`, with no ceiling of its own. A raw hatch is the most plausible place to want a bigger one and still the wrong place to grant it — nothing anybody reads at a terminal is within an order of magnitude of eight mebibytes, the one case that could genuinely exceed it is a patch blob, which `pr diff --max-bytes` already reads properly, and a per-command ceiling would be the single exception to "atgc holds at most this much of a stranger's response" on the command most likely to be pointed at a stranger. The public arm stops mid-stream through `bytes_bounded`; the authenticated arm is the entry below