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 <nsid> is the hatch.
The design question was which credential, and the answer is that the host
decides: --host pds|knot:<hostname>|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 #
api — an authenticated raw XRPC escape hatch, atgc api <nsid>.
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:<hostname>|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
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
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
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