at-intents docs agent-guide.md
12 kB · 224 lines
Markdown
at main

Agent guide — author AT Intents capability records for a repo #

Audience: an AI coding agent (Claude Code or similar) working inside an app's repository. A developer has said something like:

"Let's make an AT Intents capability record — see https://github.com/<...>/at-intents for context."

This document is your instruction set. Your job: scan the repo, figure out what the app does, write capability record file(s) as JSON, and tell the developer the exact CLI command to publish them. You do not publish records yourself (that needs the app's credentials) and you do not need to run anything that writes.

If you can fetch additional context, read overview.md and capability-spec.md in this repo. The essentials are inlined below so you can work from this file alone.

The model in 60 seconds #

AT Intents lets an app declare what it can do so other apps and agents can discover it from a user's repo. Each capability is a (verb, subject, handler) triple plus a delivery method, stored as a dev.at-intent.capability record in the app's own repo.

  • verb — share save subscribe annotate open (open set; pick the closest, or coin one and flag it to the developer).
  • subject — what it acts on, a union of shapes: uri (a bare web/feed URL), at-uri (a record, optionally of a lexicon type), did (an account).
  • delivery — passive (you read a record already in the user's repo), repo (the consumer writes a record into the user's repo that you read), or service (the consumer must call your XRPC endpoint; a PDS write won't reach you). This is the field most likely to be wrong — get it right. A service endpoint is an XRPC method (https://host/xrpc/<nsid>); see service shape.

Your procedure #

Step 1 — confirm scope with the developer #

Briefly state what you're about to do and ask one question if anything is ambiguous: "I'll scan the repo for user-facing actions and draft AT Intents capability specs. Should I cover all of them or a specific feature?" Then proceed.

Step 2 — scan the repo for what the app does #

You are looking for user-facing actions and the data each one touches. Gather evidence; don't guess. High-signal places to look:

App identity (for name / icon / the app DID):

  • package.json name/description, README*, site <title>/og: tags, public//static/ for an icon, manifest.json, marketing copy.
  • The app's atproto identity: a did:web:<domain>, a configured handle, OAuth client-metadata.json, or .well-known/atproto-did. The capability records live in this app's repo, so the DID is the app's own.

Lexicons the app defines or writes (for subject / produces):

  • lexicons/**/*.json (files with "lexicon": 1) — record types this app owns.
  • atproto write calls: grep for com.atproto.repo.createRecord, putRecord, applyWrites, .create(, agent.com.atproto.repo, or an SDK wrapper. The collection / $type argument is a produces NSID.
  • Record types it reads but doesn't own (e.g. community.lexicon.bookmarks.bookmark, app.bsky.feed.post) → candidate passive subjects.

Endpoints / backends (to decide delivery):

  • XRPC routes or HTTP handlers (/xrpc/<nsid>, an API router, server routes, serverless functions) — a service endpoint.
  • A non-PDS canonical store (a database — D1, Postgres, KV; an external API) that user actions write to. If the action's source of truth is a backend, not the user's PDS, it's delivery: service.
  • Note whether the route is already XRPC-shaped (/xrpc/<nsid>, params in the query string, { error, message } errors). If the action's endpoint is a plain REST/RPC route (POST /api/save, a GraphQL field, a ?action= handler), flag it — see below.

The actions themselves (for verb / subject):

  • UI: buttons/menus labeled save, subscribe/follow, share/post, highlight/annotate, open/view. Share targets, intent handlers, "add to…" flows.
  • Functions/routes named save, subscribe, share, post, bookmark, highlight, follow, import.

Step 3 — map each action to a capability #

For every distinct user-facing action, decide the triple + delivery using this table:

Question Field How to answer from evidence
What's the action? verb save / subscribe / share / annotate / open — closest match.
What can you act on? subject[] A raw URL → {kind: uri}. A record you accept → {kind: at-uri, of: <nsid>}. An account → {kind: did, as: account}. List every accepted shape.
Where does the result live? delivery In the user's PDS as a record you write/read → repo. In your backend → service. You only read an existing repo record → passive.
(repo) what record? produces The NSID(s) from the createRecord calls.
(repo) what permission? scope include:<your.permission.set.nsid> — ask the developer if unknown; leave a clear TODO.
(service) what URL? endpoint The XRPC method URL (https://host/xrpc/<nsid>) that performs it. If the real route isn't XRPC-shaped, see below.
(service) DID-doc fragment? serviceId The app's service-endpoint fragment id in its DID document (e.g. chirp_api), if it publishes one. Lets consumers reach the method by service proxying (atproto-proxy: <did>#<serviceId>) instead of a direct cross-origin call. Leave a TODO if unknown.
Extra inputs? input[] A note, title, target collection — name + kind.

The delivery decision is the one that silently breaks if wrong. A "save" whose data lands in your own database is service, not repo — a PDS write would never reach your backend, so a consumer would think it saved when it didn't. When the evidence is ambiguous, write your best guess and call it out in the summary for the developer to confirm.

Write description for a planner and a human: what it does, when to use it, what it won't do (≤300 graphemes). This text drives both UI pickers and agent tool-calling, so it matters.

Service must be XRPC-shaped #

A delivery: service endpoint is an XRPC method — an absolute URL whose path is /xrpc/<nsid>, with the subject and inputs carried as XRPC parameters (scalars in the query string, blobs in the request body) and errors returned as { error, message } JSON. The verb picks the HTTP method: open → GET query, every other verb → POST procedure. (Full rules: service wire contract.)

If the app's real endpoint isn't XRPC-shaped — don't quietly paper over it. A POST /api/save, a GraphQL mutation, a ?action=save handler, or any plain REST/RPC route can't be invoked by a consumer through this contract. When you find one backing a service capability:

  1. Write the capability with the XRPC endpoint it should have (https://host/xrpc/<app.nsid.verb>) and mark it TODO — don't invent a route that doesn't exist yet and present it as real.
  2. Suggest the app expose an XRPC method for that action — either a thin /xrpc/<nsid> wrapper in front of the existing handler, or by moving the route onto the app's XRPC server. Spell out the small delta (path → /xrpc/<nsid>, read params from the query string, return { error, message } on failure).
  3. Call it out in the Step 6 summary as a required change before the service capability will actually work, not just a style nit.

Getting this right is what keeps a service capability honest: a non-XRPC route publishes fine but no consumer can route to it, so it "looks like it worked" while being unreachable — the same silent-failure trap as a mislabeled repo.

Step 4 — write the record file(s) #

Create one JSON file in the repo holding the capability record(s) — a single object, or { "capabilities": [ ... ] } for several. It's the literal dev.at-intent.capability shape; $type and createdAt are filled in by the writer, so you can omit them. Recommended path: at-intent/capabilities.json (or alongside the app's lexicons). Any _-prefixed field (e.g. _comment) is ignored, so use it for review notes.

{
  "_comment": "Drafted by an agent from repo analysis. Review delivery/scope/endpoint, then publish with the CLI below.",
  "capabilities": [
    {
      "name": "<app display name>",
      "icon": "<https icon url, if found>",
      "description": "<what it does, when to use it, what it won't do>",
      "verb": "<save|subscribe|share|annotate|open>",
      "subject": [
        { "kind": "<uri|at-uri|did>" }
      ],
      "delivery": "<passive|repo|service>",

      "_delivery_note": "Keep ONLY the fields for the delivery you chose:",
      "_passive": "you read an existing repo record — no produces/scope/endpoint",
      "_repo": "consumer writes a record you read — set produces + scope",
      "produces": ["<nsid you write — repo only>"],
      "scope": "include:<your.permission.set>   TODO confirm — repo only",
      "_service": "consumer calls your backend — set endpoint (+ serviceId for proxying) instead",
      "endpoint": "<https url / xrpc route>   TODO confirm — service only",
      "serviceId": "<DID-doc service fragment, e.g. app_api>   optional — service only, enables proxying"
    }
  ]
}

Fill it from your evidence and delete the _* hint fields and any delivery-fields that don't apply. Mark every value you guessed with a TODO note (a _todo field, or call it out in your summary).

Step 5 — tell the developer how to publish #

Do not write records yourself. Give the developer this, adjusting the file path and identifier (the tooling is zero-dependency Node 18+ — no install):

cd tools

# 1) Dry-run — validates and prints the exact write, writes nothing:
node write-capability.mjs <path>/capabilities.json --dry-run --identifier <app-handle>

# 2) Publish to the APP's repo (use an app password, never the main password):
PDS_IDENTIFIER=<app-handle> PDS_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
  node write-capability.mjs <path>/capabilities.json

Also mention the interactive builder as an alternative: node write-capability-interactive.mjs.

Step 6 — summarize for review #

End with a short report:

  1. Capabilities drafted — one line each: verb + subject + delivery.
  2. Assumptions & TODOs — every value you guessed, especially delivery, scope, and endpoint, with where the evidence was thin. Flag any service action whose real route isn't XRPC-shaped and recommend exposing an /xrpc/<nsid> method (see Service must be XRPC-shaped) — call it a blocker, since the capability can't be routed to until then.
  3. Usage records — remind the developer that discovery also requires writing a dev.at-intent.usage record into each user's repo when they use the app (see producers.md); that's app code, not a one-time publish.
  4. Next command — the dry-run line from Step 5.

Guardrails #

  • Don't invent NSIDs, endpoints, scopes, or a DID. If the repo doesn't show it, leave a TODO and ask. A wrong produces/endpoint is worse than a blank one.
  • A service endpoint must be XRPC (/xrpc/<nsid>). If the app's real route isn't, recommend exposing one rather than pointing endpoint at a non-XRPC URL (see Service must be XRPC-shaped).
  • A capability may only describe its own app's records/endpoints — never another app's collections. (This is also a security rule: descriptors are untrusted input to consumer agents.)
  • Don't publish anything. Your deliverable is JSON record files + the command. The human runs the write, ideally --dry-run first.
  • Prefer fewer, correct capabilities over many speculative ones. One verb you verified beats five you inferred.