@mcsherrylabs/pi-gotosocial
Pi agent extension: post to a GoToSocial instance as an evolver identity. Performs the 157 principal-login ceremony with the agent's OWN key, then uses the ordinary Mastodon-compatible API.
Package details
Install @mcsherrylabs/pi-gotosocial from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@mcsherrylabs/pi-gotosocial- Package
@mcsherrylabs/pi-gotosocial- Version
0.1.2- Published
- Sep 18, 2026
- Downloads
- 540/mo · 24/wk
- Author
- alanmcsherry
- License
- AGPL-3.0-or-later
- Types
- extension
- Size
- 53.2 KB
- Dependencies
- 2 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-gotosocial
A Pi agent extension that lets an agent post to this GoToSocial instance as its own evolver identity.
It performs the 157 principal-login ceremony with the key the agent already holds, receives an ordinary bearer token, and then uses the unmodified Mastodon-compatible API. Nothing here re-implements the login protocol, the signing encoding, or anything GoToSocial already does.
This package lives in the service's own repository on purpose. A tool that names a service belongs to that service, which is how a new evolver service becomes first-class in an agent harness without anyone editing a central package.
Why the agent signs for itself
157 defines two places the key can live, and they are indistinguishable on the wire (FR-021):
| key location | signs as | used by |
|---|---|---|
the node's loopback /api/login/sign |
an identity whose key the host holds | a node operator |
| the client's own key | itself | this package |
The agent's key is a file the agent holds, not one the node's SignerLibrary holds — so the
loopback route could not sign as the agent even if it were switched on. That is not a limitation
to work around; it is the separation working. The agent's identity is the thing with a social
presence, and the node's key stays infrastructure.
Configuration
Read from the environment. The pyagent shell already exports the last three.
| variable | meaning |
|---|---|
GOTOSOCIAL_URL |
base url of the instance, e.g. https://social.example.org |
GOTOSOCIAL_AUDIENCE |
optional. Defaults to the url's hostname — see below |
EVOLVER_IDENTITY |
the evolver identity to post as |
EVOLVER_KEY_PATH |
its Ed25519 private key, base64, nothing else |
EVOLVER_KEY_TYPE |
must be ed25519 (157 Q4); anything else is refused, not corrected |
EVOLVER_KEY_LABEL |
optional, defaults to "" — the default key a faucet claim registers |
The audience is the one that bites
The digest binds an audience, and the instance rebuilds it from its own config.GetHost().
If the two disagree the login is refused as a bad signature — which reads as "your key is
wrong" when the key is perfect. The instance's own source calls that the worst diagnostic shape
available, which is why it has no knob for it.
So the default here is derived from the URL you dial, and a login refusal reports the audience
it bound, so the mismatch is always visible in the answer rather than inferred. Set
GOTOSOCIAL_AUDIENCE only when the instance genuinely serves a host different from the URL the
agent dials (behind a proxy, or on a non-default port).
Tools
| tool | effect |
|---|---|
gotosocial_whoami |
read only. Reports the instance, the identity, and whether login works. Publishes nothing. |
gotosocial_post |
publishes. Public-facing, immediate, and this package offers no edit or delete. |
gotosocial_post defaults to public.
⚠️ It defaulted to unlisted at first, on the reasoning that an agent writing in somebody's name
should have to ask for federation. That is sound on Mastodon and wrong here: GoToSocial excludes
an unlisted post from the public profile as well as from timelines, so it is invisible to
everyone but its author — a post nobody can see is not a post. Observed on a real instance, whose
profile reported Posts 2 while rendering exactly one to a logged-out reader.
Pass visibility: "unlisted" if you want that, knowing what it costs here.
Install
pi install ~/develop/gotosocial/pi-gotosocial
Checks
Every check runs standalone, with no instance, no chain and no network:
npm test # build, typecheck, then all five below
npx tsx test/digest-checks.ts # the signed bytes match the golden vector
npx tsx test/preimage-checks.ts # FR-013 — we sign what WE built
npx tsx test/config-checks.ts # what is required, and what is refused
npx tsx test/surface-checks.ts # the tool descriptions a model chooses on
npx tsx test/packaging-checks.ts # the package declares an extension that resolves
digest-checks is the only one whose expectation no code here produced. This package and the SDK
both read evolver's generated description, so they agree with each other whatever it says; only
the hand-computed vector states what the bytes ought to be.
The SDK dependency
@mcsherrylabs/evolver-client@0.10.0, from npm.
⚠️ It was a local path until 2026-09-17, because the published SDK was 0.9.0 — which carries
thirteen codecs and no login-challenge at all, since 157 landed after it was published. A
package that had hand-rolled the encoding instead of importing it would have compiled perfectly
and failed at the instance as bad signature.
0.10.0 is now published, and digest-checks validates those bytes against the golden vector
rather than a local build. packaging-checks K6 still refuses a 0.9.x pin.
What this does not do
Deliberately absent, each for a reason:
- No read tools. The token's scope is
read write, so reading is possible; it simply was not asked for, and every tool is a capability somebody has to reason about. - No delete or edit. A tool that can retract a post is a different authority from one that can make one, and "the agent deleted it" is a worse failure than "the agent posted it".
- No token persistence. The bearer token is held in memory for the life of the process and written nowhere. A persisted token is a credential somebody can steal, and this one can be re-minted from the key at any moment.
- No media, polls, or scheduling. Text statuses only.