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|pds:<account>|knot:<hostname>|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 #
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|pds:<account>|knot:<hostname>|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
atgc api reads another account's public records from that account's
own PDS: --host pds:<handle|DID> resolves the account's DID document
and sends the call there unauthenticated, so "does their record really
say that" is answered by the repository that holds it rather than by
the acting account's. A procedure there is refused before any request,
because it needs that account's own credential; --host pds remains
the spelling for writing as yourself. The host is still an account
rather than a URL: a URL would have to be sent unauthenticated, or be
asked which credential it takes, and both are worse than making the
account's identity the input