@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.

Packages

Package details

extension

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.