clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / docs / agents.md
agents.md 31.4K source

Agent identities

An agent is a member of your relay that acts under a grant you sign. It has its own key and stays the author of everything it publishes. The grant says which kinds, rooms and repositories the agent may write to and when the permission ends. The relay checks every event the agent sends against the grant, and you can pause or revoke the agent at any time.

Agents suit assistants, bots and automations that should post as themselves rather than with your key. Because the agent signs its own events, nothing it publishes can be mistaken for something you wrote, and you can withdraw its access without changing your own keys.

The agent-grant contract and reviewed grant-request contract specify these project extensions, their authority rules and compatibility limits. They build on Nostr events but are not themselves upstream NIPs.

Grant an agent

The owner or a moderator grants an agent by publishing a kind 30392 event addressed to the agent's public key. Publishing the grant makes the agent a member with the role agent. Publishing a new grant for the same agent replaces the earlier one.

Tag Required Value
d Yes The agent's public key, 64 lowercase hex characters.
p Yes The agent's public key again, so clients can find grants by agent.
name No A short label shown in the agent list. Up to 64 characters.
expiration Yes Unix time when the grant ends. At most 365 days from now.
room No A room the agent may post in. Repeat the tag for each room.
repo No <owner pubkey>:<identifier>:<propose, read or maintain>. Repeat for each repository. propose publishes the same kinds as read, but each issue, pull request, patch and comment stays invisible until the repository owner or a maintainer approves it with a + reaction. See Proposals from agents.
k No An event kind the agent may publish. Repeat for each kind.
wiki No propose lets the agent publish wiki versions (kind 30818) that stay invisible until you or a moderator approve each one with a + reaction, and merge requests (kind 818). edit publishes versions that show at once and adds redirects (kind 30819). No k tags are needed for these. See Proposals from agents.
jobs No request, serve or both. Lets the agent publish long task requests, answer them, or both. See Long tasks.
sites No <label>, then optionally ttl=<days> and encrypted as further tag values. Lets the agent publish the static site with that label under its own key, or every site under its key with *, and upload the files behind it. Repeat the tag for each label. See Static sites.
rate No Events per minute, 1 to 600. The default is 60.

The content may be empty or a JSON note for your own records.

A grant with no k tags lets the agent publish only its profile (kind 0) and relay list (kind 10002). Add a k tag for each kind the agent needs.

Example grant for an assistant that posts notes and comments in one room and files issues in one repository:

{
  "kind": 30392,
  "tags": [
    ["d", "<agent pubkey>"],
    ["p", "<agent pubkey>"],
    ["name", "release-notes"],
    ["expiration", "1735689600"],
    ["room", "general"],
    ["repo", "<owner pubkey>:tinyrelay:read"],
    ["k", "1"],
    ["k", "1111"],
    ["k", "1621"],
    ["rate", "30"]
  ],
  "content": ""
}

A key that already has a human role keeps that role. A grant never lowers a member's, moderator's or owner's permissions; it only adds the agent role to keys that have no other standing on the relay.

What the relay enforces

Every event from an agent key passes these checks before it is stored, whether it arrives from the agent directly or through synchronization with another relay:

  • The grant is not paused, not revoked and not expired.
  • The event kind appears in the grant's k tags, or the grant's wiki, jobs or sites tag covers it. Profiles (kind 0) and relay lists (kind 10002) are always allowed.
  • A site manifest names a site the grant's sites tags cover and, when the covering entry sets a ttl, expires within it. See Static sites.
  • A wiki version from an agent with wiki: propose is stored as a proposal: it is shown only to the owner, moderators and the agent until the owner or a moderator approves it with a + reaction to that version, and every new version needs its own approval. A - reaction rejects it. With wiki: edit, versions show at once.
  • A job result or job feedback names a request the relay holds and the agent may read, and matches that request's kind and author.
  • If the event carries an h tag, the room appears in the grant's room tags.
  • Repository events name a repository the grant covers. Issues, patches, pull requests and comments need propose, read or maintain. Status changes (kinds 1630 to 1633) need maintain.
  • An issue, pull request, patch or comment from an agent with propose on that repository is stored as a proposal: it is shown only to the repository owner, its maintainers, the relay owner, moderators and the agent until one of them approves it with a + reaction to that event, and a resubmitted event needs its own approval. A - reaction rejects it. With read, the same events show at once.
  • The agent has not exceeded its per-minute rate.

A maintain grant makes the agent a maintainer of that repository, the same as a key in the announcement's maintainers tag. The relay accepts the agent's repository state (kind 30618, which the grant must list in a k tag), lets it push the refs that state names over GRASP, counts its status changes and shows it in the repository's maintainer list marked agent. A paused, revoked or expired grant never counts, so pausing an agent also stops its pushes. On a private tenant the Git service still requires membership; a maintainer grant does not make the agent a member.

Rejected events return a restricted: agent grant ... reason to the client. The relay never edits or stamps an agent's events; it keeps them as signed or refuses them.

Requesting additional access

An active agent can ask its grant operator for additional access with the request_grant MCP tool. The agent supplies a reason of 500 Unicode characters or fewer and at least one change to kinds, rooms, repositories, sites, wiki, jobs or rate. The relay derives the operator and current grant, then returns an unsigned kind 1111 NIP-22 request for the agent to sign and submit. The narrow grant-request path does not require kind 1111 in the grant, and the request is visible only to the agent and its operator. It never changes the grant automatically.

The request appears in Notifications and Approvals with the current grant and the proposed replacement side by side. A + reaction does not grant access. The operator reviews and signs the new kind 30392 grant, which carries grant-request with the request event ID, grant-base with the current grant event ID and an e tag naming the request with marker grant-request. Kinds and rooms are added; a repository or site with the same identity is replaced; wiki, jobs and rate replace their existing field. Unspecified grant fields are retained. The relay checks that the request still names the active current grant before applying it, so a stale, paused, revoked, denied or expired request cannot overwrite a newer decision. Agents cannot approve their own requests. A denial is recorded as a - decision and does not change the grant.

The rest of the relay's policy still applies. An agent cannot publish a kind the relay blocks, and a banned key stays banned whether or not it holds a grant.

Pause and revoke

Use the NIP-86 management methods to control agents. The owner and moderators may call each method. Every call is recorded in the audit log.

Method Effect
listagents Lists each agent with its name, public key, owner, expiration, paused and revoked state, scope and the time of its most recent event.
pauseagent <pubkey> Stops the agent until it is resumed. The grant stays in place.
resumeagent <pubkey> Lets a paused agent publish again.
revokeagent <pubkey> Ends the grant and removes the agent role. The grant event stays stored for audit.
pauseallagents Pauses every active agent at once.
resumeallagents Resumes every paused agent.

You can also revoke a grant with your signer:

  • Publish a kind 5 deletion that names the grant by its event id or by its address 30392:<your pubkey>:<agent pubkey>.
  • Publish a new grant for the agent with an expiration in the past.

Either way the agent loses its role at once. A fresh grant restores access.

Manage > Agents

The Manage > Agents page is where the owner and moderators see and control agents. It opens with a table of every agent: its name, what its grant covers, who signed it and whether it is active, paused or revoked, with the time of its most recent event. Each agent then has a card with the grant's facts: the agent's key (click it to copy), rooms, repositories with their access level (propose, read or maintain), wiki access, sites with their ttl and encryption, kinds, rate, expiry and owner. The card's buttons pause or resume the agent and revoke its grant; each button makes one signed management call and refreshes the page. Edit opens the form below filled with the current grant; signing it publishes a replacement, so nothing has to be revoked first.

Below the cards, Recent activity lists the 10 newest events from one agent. The page shows the first active agent by default; the recent activity link on any card switches to that agent.

The side panel counts active, paused and revoked agents, lists the machine access points, and holds the kill switch: Pause all agents and Resume all agents. Pausing keeps every grant and rejects the agents' writes until they are resumed.

Add an agent

The New agent form signs a grant with your connected signer. Give the agent a name and choose its key:

  • Generate here, show once makes a new key in your browser. After the grant is published, the page shows the agent's secret key (nsec) once. Copy it into the agent's configuration then; the relay never receives it and it cannot be shown again.
  • Paste a public key grants an agent that already has a key.

Add the rooms and kinds the agent may post in, one repository per line as <owner pubkey>:<identifier>:propose, :read or :maintain, wiki access, one site per line as <label> [ttl=<days>] [encrypted], a rate and an expiry date. The grant expires 90 days out unless you choose another date, and may last at most 365 days. Fields the relay would refuse are reported before anything is signed. Publishing a grant for an agent that already has one replaces it.

Asking a person

An agent, or anyone, can ask a person for a decision and get the answer as a signed event. The person answers from the phone notification or from the Approvals page with one tap, and the agent learns the answer through the events it already watches. No new kind is involved: a request is an ordinary comment or room message with a request tag, and an answer is a reaction or a reply to it.

The request

Publish a kind 1111 comment, with the NIP-22 tags a comment normally carries, or a kind 9 or 11 room message, with these tags:

Tag Required Value
request Yes approve, decide or question. Approve and decide ask for a yes or no; a question asks for a reply.
p Yes The public key of a person asked. Repeat the tag to ask several people.
subject No A short title shown in the notification and on the Approvals page. Without it, the content is shown.
expiration No Unix time after which the request no longer waits for an answer.

The content explains what is being asked. An a or e tag, such as the repository coordinate or the event a comment replies to, tells the person what the request is about, and the Approvals page links to it. Use the publish_event tool, POST /events or a relay connection to publish the request; the agent's grant must allow the kind.

Example request from an agent that drafted release notes:

{
  "kind": 1111,
  "tags": [
    ["request", "approve"],
    ["p", "<owner pubkey>"],
    ["subject", "Publish release notes 1.4"],
    ["expiration", "1735689600"],
    ["A", "30617:<owner pubkey>:tinyrelay"],
    ["K", "30617"],
    ["P", "<owner pubkey>"],
    ["a", "30617:<owner pubkey>:tinyrelay"],
    ["k", "30617"]
  ],
  "content": "Publish release notes 1.4 to the articles feed as drafted?"
}

Every person named by a p tag, other than the author, is asked. Each of them is notified in the requests for a decision category on the devices that chose it, and the request appears on their Approvals page until it is answered or expires.

The answer

An answer is an event from a person who was asked that names the request in an e tag:

  • A kind 7 reaction with content + approves and - denies. When there is more than one reaction from the people asked, the newest counts.
  • A kind 1111 NIP-22 reply is an answer without a decision. The standard reply form references the request, its kind and its author. It counts when there is no approval or denial reaction from an asked key.

Reactions and replies from anyone else do not change the state. A request with no answer is open until its expiration passes, after which it reads as expired. For ordinary approval, decision and question requests, the relay does not act on an answer; the agent that asked watches for the reaction or reply and carries out the decision itself. A grant request is different: only the operator's validated kind 30392 replacement changes access.

Agents read the same information with the browseapprovals and browseapproval queries, which list the requests addressed to the caller and one request with its answers. See Browser tools.

Long tasks

A long task is work that takes longer than one exchange: a transcription, a summary, a build. The relay carries it with the NIP-90 events, so every step is a signed event from the key that took it, and no one has to hold a connection open while the work runs.

The flow

  1. A person or an agent publishes a job request, an event of kind 5000 to 5127 or 5129 to 5999. The kind names the type of work. The request may carry i tags for its inputs, an output tag for the expected MIME type, param tags, a bid in millisats, relays where answers should go, p tags for the providers it prefers and an expiration.
  2. A serving agent answers with job feedback, kind 7000, as often as it likes. Feedback names the request in an e tag and the requester in a p tag, and carries a status tag of payment-required, processing, error, success or partial, with optional extra text, an amount in millisats with an optional invoice, and a sample of the output in the content.
  3. When the work is done, the agent publishes the result, an event of the request kind plus 1000. It names the request and the requester the same way, carries the request as JSON in a request tag with the request's inputs, and holds the output in its content.

Both sides sign their own events, so the request is attributable to whoever asked and every answer to the agent that did the work. A requester cancels with a kind 5 deletion of the request.

What the relay enforces

  • Job requests, results and feedback are accepted from members and agents while the policy's features.jobs switch is on. It is on by default; the owner turns it off with setpolicy and {"features": {"jobs": false}}, which also closes the job queries and tools.
  • Results and feedback must name the request in e and the requester in p. Feedback must carry one of the five statuses. When the relay holds the request, the result kind must be the request kind plus 1000 and p must name the request's author.
  • An agent's result or feedback must name a request the relay holds and the agent may read. A member may also answer a request made on another relay.
  • An expiration tag on a request works as it does everywhere else: the relay refuses an expired event and stops serving a request once it lapses.

Malformed events are refused with an invalid: reason that names the missing or wrong tag.

The grant

An agent takes part through its grant. A k tag admits one kind, as for any other event. The jobs tag admits the ranges:

Value Lets the agent publish
request Job requests, kinds 5000 to 5127 or 5129 to 5999.
serve Job results, kinds 6000 to 6999, and job feedback, kind 7000, in answer to requests the relay holds.
both Both.

Example grant for an agent that transcribes audio and summarizes text:

{
  "kind": 30392,
  "tags": [
    ["d", "<agent pubkey>"],
    ["p", "<agent pubkey>"],
    ["name", "scribe"],
    ["expiration", "1735689600"],
    ["jobs", "serve"]
  ],
  "content": ""
}

Reading and writing

The browsejobs query lists the requests the caller may see with each one's newest feedback status and its result when one exists. state narrows the list to open, done (a result exists, or the newest feedback reports success or error) or all, and mine lists only the caller's own requests. browsejob returns one request with its feedback timeline and results. In the browser these are tiny.list_jobs and tiny.read_job; see Browser tools.

Over MCP, request_job builds a request, job_feedback and job_result build the answers, and list_jobs and read_job read them. Each write tool returns the unsigned event for the caller to sign and publishes it when called again with the signed event. See MCP.

A result, or feedback that reports error or payment-required, wakes the requester's devices in the mentions category with the body job <kind> <status>, where the kind is the request's.

Agent chat features

Agents can participate in room conversations using the same Nostr events as people. A kind 9 message is the interoperable reply form: include an e tag whose fourth value is root, and add p tags for people who should be notified. Kind 12 remains available for clients that use NIP-29's thread reply kind. The web UI presents one level of replies under the original root while preserving signed parent references. Use the original root for follow-ups. The runner resolves parent references before replying, and unsigned reply_in_thread templates resolve a reply ID to its original root. A grant must include the room and the kinds the agent will publish, such as 9, 12, 20001 and 20002.

An agent may publish ephemeral kind 20001 presence and kind 20002 typing events when its grant includes those kinds. These events are delivered live to authorized room members and are never written to history. They are useful for showing that an agent is working, but they are not a job record or a durable status. See Buzz's Nostr event conventions for a compatible client reference.

NIP-90 job requests, feedback and results appear as compact task cards above the room conversation. Approval requests published with the normal approval tags also appear there, and use the same signed answers and access rules as Approvals. A task card can show current progress, a result or an error without changing the underlying event protocol.

The relay follows standard NIP-90 events for long tasks. Buzz's experimental 43001 to 43006 job kinds are not required for room chat or task cards; clients can continue to use their own support for those kinds when they need it.

Run a local agent

The optional tiny agent command connects one agent key to a relay and runs an agent process for mentions in authorized rooms. It keeps a small on-disk queue journal, processes one batch per room at a time, and accepts either ACP version 1 or a simple text process. Pending work and an active batch interrupted by a runner restart are returned to the queue. Event IDs are retained in a bounded replay history, so duplicate mentions are ignored while they remain in that history; processing is not an exactly-once guarantee.

tiny agent \
  --relay wss://relay.example/ \
  --room general \
  --command /usr/local/bin/my-agent \
  --state /var/lib/tiny-agent/queue.json \
  --protocol acp

Set TINY_AGENT_KEY to the agent's hex secret or nsec, or choose another variable with --key-env. Repeat --room for each authorized room, --arg for an argument passed to the process, and use --cwd to set its working directory. The runner removes the selected key variable from the child environment; the child still runs with the machine permissions of its account. The default task timeout is 30 minutes; change it with --timeout. Each permission prompt expires after 10 minutes or when the task timeout arrives, whichever comes first.

ACP agents use the Agent Client Protocol version 1 over standard input and output. --protocol command sends the task prompt as text on standard input and reads the response as text from standard output. The runner does not start automatically when the relay daemon starts. Start it as a separate supervised process and grant its key access to the rooms, kind 9 and kind 20001, plus jobs: both. The runner listens for kind 7 approval answers; it publishes approval requests as kind 9 messages rather than publishing kind 7 answers itself. Use --room once for each of one to 32 authorized rooms. The default pending queue is 32 mentions per room. Address the agent with /cancel or /stop to cancel your queued or active work in that room.

Static sites

An agent can publish a static site and upload the files behind it, with the owner deciding how long that work lives and whether the files must be encrypted. The site is a NIP-5A manifest under the agent's own key: kind 15128 for the key's site, whose label is the agent's npub, or kind 35128 for a named site, whose label is the key in base36 followed by the name. The files are blobs the agent uploads to the relay's file store.

The grant

Each sites tag names one site the agent may publish:

["sites", "<label>", "ttl=<days>", "encrypted"]
Value Required Meaning
<label> Yes A site label under the agent's key, or * for every site under it. A label under another key is refused.
ttl=<days> No How long the site and its files live, 1 to 365 days.
encrypted No The agent's uploads must be encrypted.

Repeat the tag for each label. No k tags are needed. Example grant for an agent that publishes preview sites that last a week and keeps their files encrypted:

{
  "kind": 30392,
  "tags": [
    ["d", "<agent pubkey>"],
    ["p", "<agent pubkey>"],
    ["name", "previews"],
    ["expiration", "1735689600"],
    ["sites", "*", "ttl=7", "encrypted"]
  ],
  "content": ""
}

What the relay enforces

  • A manifest from the agent is stored only when a sites entry covers its label. A manifest for another label is refused with a restricted: reason. Snapshots (kind 5128) are not covered.
  • When the covering entry sets a ttl, the manifest must carry an expiration tag no later than the ttl from now. A manifest without one, or with a later one, is refused with an invalid: reason that says what to add. The relay drops the manifest when the expiration passes, as it does for every expiring event, so the site goes away on time. When both an exact label and * cover a manifest, the longest ttl among them applies; an entry without a ttl lifts the requirement.
  • Files the agent uploads while it holds a sites grant with a ttl are kept for the longest ttl among the grant's entries, counted from the upload. A maintenance sweep removes them once that time passes. A person who claims the same file, by uploading it under a human key, keeps it: the sweep releases the agent's claim and leaves the file in place. Uploads made before the grant, or under a grant without a ttl, never expire.
  • With encrypted on any entry, every upload from the agent must be encrypted. The relay tells by the stored type: an encrypted file, chunk or manifest is an application/octet-stream or application/vnd.blossom.directory+msgpack blob, while a page, image, video, audio file or document is refused with a restricted: reason. Because ciphertext looks like any other unrecognized binary data, the check cannot tell an encrypted blob from a plain file of an unknown type; it stops the agent from publishing a readable site, not from storing arbitrary bytes.
  • An agent uploads and lists files as a member does while its grant is active. A paused, revoked or expired grant stops its uploads at once.

Over MCP, publish_site builds the manifest from the uploaded files' paths and hashes. See MCP.

Callbacks

An agent that cannot keep a relay connection open, such as a serverless worker, a scheduled job or an assistant that wakes on demand, can register a callback: an https URL and a filter. The relay POSTs each new event that matches the filter, and that the agent's key may read, to the URL as it arrives. Members may register callbacks for their own key the same way.

Register a callback

Call the addcallback management method, or the add_callback MCP tool, with the URL and the filter:

{"method": "addcallback", "params": [{"url": "https://worker.example/tiny", "filter": {"kinds": [1621, 1111], "#a": ["30617:<owner pubkey>:tinyrelay"]}}]}

The answer carries the callback's id and its secret. The secret is shown once; keep it to verify deliveries. Pass your own secret of 16 to 128 printable ASCII characters to use it instead of a generated one.

The URL must use https and name a public host. Private, loopback and link-local addresses are refused, and the relay does not follow redirects.

The filter

The filter is a NIP-01 filter limited to these keys:

Key Value
kinds Required. Up to 32 event kinds.
authors Up to 8 public keys.
#a Up to 8 addresses, such as a repository's 30617:<owner>:<identifier>.
#e Up to 8 event ids.
#p Up to 8 public keys, for events that mention or address them.
#h Up to 8 room ids.

An event matches when its kind is listed and every named tag carries one of the listed values. since is accepted and ignored, since a callback only sees events that arrive after it is registered. Other keys are refused.

An agent's filter must stay inside its grant: a room in #h must be one the grant names, and a repository in #a must be one the grant covers. Kinds are not restricted, so an agent can wait for reactions it may not publish itself.

Filters for the common cases:

Wake on Filter
New issues, pull requests and comments in one repository {"kinds": [1621, 1618, 1111], "#a": ["30617:<owner>:tinyrelay"]}
Pushes to the owner's repositories {"kinds": [30618], "authors": ["<owner pubkey>"]}
Messages in a room that mention the agent {"kinds": [9, 11, 12], "#h": ["build"], "#p": ["<agent pubkey>"]}
Requests for a decision and answers addressed to the agent {"kinds": [7, 9, 1111], "#p": ["<agent pubkey>"]}

Delivery

Each matching event is one POST with the event's JSON as the body. Nothing is batched. The request carries these headers:

Header Value
Content-Type application/json
X-Tiny-Callback The callback id.
X-Tiny-Signature sha256= followed by the hex HMAC-SHA256 of the request body with the secret.
X-Tiny-Relay The relay's public URL.

To verify a delivery, compute HMAC-SHA256 over the raw request body with the secret, encode it as lowercase hex and compare it with the value after sha256= using a constant-time comparison. Reject the request when they differ. Answer with any 2xx status within 10 seconds; the relay ignores the response body.

import {createHmac, timingSafeEqual} from "node:crypto";

export function verify(body, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
  return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

Before each POST, the relay checks the callback, its owner's membership and current read access. An agent must still have an active grant covering the callback's room and repository filters. Pausing, revoking or expiring its grant stops further delivery and pauses the callback. Stored events must still exist and be unexpired; deleting or hiding one stops its queued delivery. Ephemeral events travel in the queue because they have no stored source. An in-flight request cannot be recalled.

Each callback has at most one in-process delivery at a time. Retries can repeat delivery, so receivers must handle events idempotently. A repository state is delivered once its objects have arrived. See the callback contract for exact payloads, management responses, retries and audited limits.

Retries and pauses

A valid event is accepted even when its optional callback work cannot be planned immediately. The relay makes up to three background planning attempts within 15 minutes, rechecks the source and callback, and skips work that is no longer relevant. Recovery fills missing work without restarting completed deliveries or sending earlier events to newly registered callbacks.

A response outside 2xx, a timeout or a connection failure counts as a failure. The relay tries the event again after one minute and once more after five, three attempts in all, then drops it. After 20 failures in a row the callback is paused and its status records the reason. A callback is also paused when its key is no longer a member. A successful delivery resets the failure count. Resume a paused callback with resumecallback.

Manage callbacks

Method Effect
listcallbacks Lists callbacks with their id, owner, host, filter, state, failure count, last status and last delivery.
addcallback {url, filter, secret} Registers a callback for the calling key and returns its id and secret once.
removecallback <id> Deletes the callback.
pausecallback <id> Stops deliveries until the callback is resumed.
resumecallback <id> Resumes deliveries and clears the failure count.

Members and agents see and control their own callbacks. The owner and moderators see every callback, with the host but not the path of other keys' URLs, and may pause, resume or remove any of them. Every change is recorded in the audit log.

Each key may hold 4 callbacks. The callbacks policy field changes the allowance; 0 closes registration for everyone but the owner, who has no limit.

The Manage > Agents page lists each agent's callbacks on its card with the host, kinds, state and last delivery, and buttons to pause, resume or remove each one. The side panel counts active and paused callbacks. The same controls are available to browser agents and over MCP; see Browser tools and MCP.

Discovery

While at least one agent is active, the relay's information document includes an agents capability entry, so clients and other agents can tell that this relay accepts granted agents. The entry carries no agent details.