| 1 | diff --git a/README.md b/README.md |
| 2 | index 278c142..3217e9f 100644 |
| 3 | --- a/README.md |
| 4 | +++ b/README.md |
| 5 | @@ -59,7 +59,7 @@ Run `tiny templates` to see available templates. Templates that read from an ups |
| 6 | |
| 7 | ## Configure and operate |
| 8 | |
| 9 | -The web interface uses plain HTML pages and forms. Connect a Nostr signer with NIP-07 or Nostr Connect to sign in and authorize management actions. **Chat** combines group rooms with one-to-one conversations. Use the management pages to configure policy, members, relay connections, delivery jobs, backups, hosted sites, and stored files. Delivery and retry status lives under **Manage > Sync**. |
| 10 | +The web interface uses plain HTML pages and forms. Connect a Nostr signer with NIP-07 or Nostr Connect to sign in and authorize management actions. **Chat** combines group rooms with one-to-one conversations. **Account** keeps the current key's profile, relay lists and Chat presence preference together. Use the management pages to configure policy, members, relay connections, delivery jobs, backups, hosted sites, and stored files. Delivery and retry status lives under **Manage > Sync**. |
| 11 | |
| 12 | The browsing pages provide repository lists, source files, branches, history, diffs, file previews, raw downloads, synchronization status, and storage status. Access follows the tenant's current permissions. |
| 13 | |
| 14 | diff --git a/docs/README.md b/docs/README.md |
| 15 | new file mode 100644 |
| 16 | index 0000000..345cafe |
| 17 | --- /dev/null |
| 18 | +++ b/docs/README.md |
| 19 | @@ -0,0 +1,36 @@ |
| 20 | +# Guides |
| 21 | + |
| 22 | +Tiny combines a Nostr relay with pages for publishing, conversation, files and collaboration. These guides describe the built-in client and the signed protocols other clients and agents use. Each tenant's policy controls which features and actions are available. |
| 23 | + |
| 24 | +## Use the relay |
| 25 | + |
| 26 | +| Task | Guide | |
| 27 | +| --- | --- | |
| 28 | +| Set up a personal relay and connect clients | [Personal relay](personal-relay.md) | |
| 29 | +| Join, invite people and manage membership | [Membership](membership.md) | |
| 30 | +| Manage your profile, relay lists, presence and device notifications | [Account and installed app](app.md) | |
| 31 | +| Use group Chat and encrypted one-to-one messages | [Chat](rooms.md) | |
| 32 | +| Publish notes and articles, comment and react | [Social](social.md) | |
| 33 | +| Upload, organize and preview files; choose public, member or secret-link access | [Files and private repositories](files-and-private-repositories.md) | |
| 34 | +| Publish wiki pages and review proposals or merges | [Wiki](wiki.md) | |
| 35 | +| Host repositories and collaborate on issues and pull requests | [Git collaboration](git-collaboration.md) | |
| 36 | +| Render supported content with templates and custom views | [Views](views.md) and [view definitions](view-definitions.md) | |
| 37 | + |
| 38 | +## Connect an agent |
| 39 | + |
| 40 | +Start with the relay's `/llms.txt` endpoint for its URLs, authentication rules, tool inventory and feature guides. It includes the tenant prefix when opened at `/r/<tenant>/llms.txt`. Use MCP `tools/list` for current argument schemas. |
| 41 | + |
| 42 | +| Task | Guide | |
| 43 | +| --- | --- | |
| 44 | +| Authenticate and call tools, upload attachments or publish sites | [MCP](mcp.md) | |
| 45 | +| Grant an agent access, request missing permissions, ask for decisions or run tasks | [Agents](agents.md) | |
| 46 | +| Use tools through a connected browser signer | [Browser tools](webmcp.md) | |
| 47 | + |
| 48 | +Agents sign with their own keys. A request for more access waits for the current operator's signed grant; a reaction alone does not change permissions. The optional `tiny agent` runner starts separately from the relay daemon. |
| 49 | + |
| 50 | +## Develop and operate |
| 51 | + |
| 52 | +- [Architecture](architecture.md) explains service ownership and event flow. |
| 53 | +- [Fixi and Paxi](fixi.md) explains page navigation, partial refreshes and signed forms. |
| 54 | +- [Testing](testing.md) covers local checks, browser fixtures, protocol conformance and Linux testing. |
| 55 | +- [Observability](observability.md) covers health, metrics, logs and profiling. |
| 56 | diff --git a/docs/agents.md b/docs/agents.md |
| 57 | index 0cc08e2..6d5cabd 100644 |
| 58 | --- a/docs/agents.md |
| 59 | +++ b/docs/agents.md |
| 60 | @@ -69,9 +69,9 @@ Rejected events return a `restricted: agent grant ...` reason to the client. The |
| 61 | |
| 62 | ### Requesting additional access |
| 63 | |
| 64 | -An active agent can ask its grant operator for additional access with the `request_grant` MCP tool. The agent supplies a reason of 500 characters or fewer and additive changes to kinds, rooms, repositories, sites, wiki, jobs or rate. The relay addresses the request to the operator on the current grant and publishes it as a kind 1111 NIP-22 request. It never changes the grant automatically. |
| 65 | +An active agent can ask its grant operator for additional access with the [`request_grant` MCP tool](mcp.md#requesting-grant-access). 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. |
| 66 | |
| 67 | -The request appears in Notifications and Approvals with the current grant and the proposed replacement side by side. The operator reviews and signs the new kind 30392 grant. The relay checks that the request still names the current grant before applying it, so an older request cannot overwrite a newer decision. Agents cannot approve their own requests. |
| 68 | +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. |
| 69 | |
| 70 | 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. |
| 71 | |
| 72 | @@ -156,9 +156,9 @@ Every person named by a `p` tag, other than the author, is asked. Each of them i |
| 73 | An answer is an event from a person who was asked that names the request in an `e` tag: |
| 74 | |
| 75 | - A kind 7 reaction with content `+` approves and `-` denies. When there is more than one reaction from the people asked, the newest counts. |
| 76 | -- A kind 1111 reply is an answer without a decision. It counts when no reaction exists. |
| 77 | +- 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. |
| 78 | |
| 79 | -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. The relay does not act on an answer; the agent that asked watches for the reaction or reply and carries out the decision itself. |
| 80 | +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. |
| 81 | |
| 82 | 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](webmcp.md). |
| 83 | |
| 84 | @@ -229,7 +229,7 @@ The relay follows standard NIP-90 events for long tasks. Buzz's experimental 430 |
| 85 | |
| 86 | ## Run a local agent |
| 87 | |
| 88 | -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 work interrupted by a runner restart are returned to the queue; a task that completed before the journal was saved is deduplicated by event id. |
| 89 | +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. |
| 90 | |
| 91 | ```sh |
| 92 | tiny agent \ |
| 93 | @@ -240,7 +240,7 @@ tiny agent \ |
| 94 | --protocol acp |
| 95 | ``` |
| 96 | |
| 97 | -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 and approval timeout is 30 minutes; change it with `--timeout`. |
| 98 | +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. |
| 99 | |
| 100 | ACP agents use the [Agent Client Protocol](https://agentclientprotocol.com/) 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. |
| 101 | |
| 102 | diff --git a/docs/app.md b/docs/app.md |
| 103 | index faf6acb..f06440f 100644 |
| 104 | --- a/docs/app.md |
| 105 | +++ b/docs/app.md |
| 106 | @@ -8,7 +8,7 @@ Share a photo, file or link from another app and choose the relay. Shared files |
| 107 | |
| 108 | ## Large uploads |
| 109 | |
| 110 | -On browsers that offer it, a file of 8 MiB or more uploads through the browser's background transfer, so the upload continues after the app closes. Every request is signed before the transfer starts, and the browser shows its own progress notice. Open the Files page later to see the result; for an encrypted file the share link appears there. On other browsers the upload runs while the page stays open. |
| 111 | +On browsers that offer Background Fetch, the Files page offers **Continue in the background** for a single file of 8 MiB or more. Choose it to let the transfer continue after the app closes; otherwise the upload stays in the page. Every request is signed before the transfer starts, and the browser shows its own progress notice. Open the Files page later to see the result; for an encrypted file the share link appears there. |
| 112 | |
| 113 | ## Nostr links |
| 114 | |
| 115 | @@ -18,23 +18,26 @@ The app registers for `web+nostr:` links. A link to an `npub`, `nprofile`, `note |
| 116 | |
| 117 | Notifications are off until you turn them on, and each device is enabled separately. Open **Chat** and choose **Enable on this device**. The browser asks for permission at that moment, and the device is registered with a signed request. Choose **Disable on this device** to stop. The owner can choose **Send a test** to check delivery. |
| 118 | |
| 119 | -Each device chooses what wakes it: private messages, replies to your posts, issues and pull requests, mentions, requests for a decision, and relay notices such as a report awaiting moderation or a finished job. A burst of activity produces one notification per category. The text is encrypted to the device, so the push service never reads it. The app icon shows how many conversation events arrived since you last opened Chat, plus the requests that still wait for your decision. Opening Chat clears the conversation count, and answering a request clears that request. Devices that stop accepting messages are removed. |
| 120 | +Each device chooses what wakes it: private messages, replies to your posts, issues and pull requests, mentions, requests for a decision, access-grant requests awaiting review, and relay notices such as a report awaiting moderation or a finished job. A burst of activity produces one notification per category. The text is encrypted to the device, so the push service never reads it. The app icon shows how many conversation events arrived since you last opened Chat, plus the requests that still wait for your decision. Opening Chat clears the conversation count, and answering a request clears that request. Devices that stop accepting messages are removed. |
| 121 | |
| 122 | A request for a decision arrives with **Approve**, **Deny** and **Reply** buttons on the notification; a question offers only **Reply**. Each button opens the request on the **Approvals** page, where Approve and Deny ask you to confirm before your signer signs the answer, and Reply opens the reply box. Nothing is signed from the notification alone. See [Asking a person](agents.md#asking-a-person) for how requests are made and answered. |
| 123 | |
| 124 | -## Names |
| 125 | +An agent's access request offers **Review access** instead. It opens the proposed grant changes in Approvals. Only the current grant operator can approve them by signing the replacement grant; the notification itself cannot authorize more access. |
| 126 | |
| 127 | -People show by name wherever the relay knows one: room authors and members, mentions, approvals, agent owners, wiki authors and the signed-in line. The name is the profile (kind 0) published on this relay, resolved in the browser by the `nostr-name` element from [nostr-web-components](https://web.nostr.technology/), which tiny vendors as one bundled script. The bundle replaces the library's profile loader with one that asks this relay only, so no public indexer learns who you look at; results are cached in the browser for six hours. Without JavaScript, or before a profile arrives, the short key shows instead, and the key stays in the element's title. |
| 128 | +## Names |
| 129 | |
| 130 | -Rebuild the bundle after changing `scripts/nostr-name-setup.mjs` or updating the package with `npm run build:nostr-name`. The package pulls its dependencies from the JSR registry, which `.npmrc` maps for the `@jsr` scope. |
| 131 | +People show by name wherever the relay knows one: room authors and members, mentions, approvals, agent owners, wiki authors and the signed-in line. Names come from the person's profile (kind 0) as held by this relay, so looking up a name does not contact a public indexer. Names are cached in the browser for six hours. Without JavaScript, or before a profile arrives, the short key shows instead, and the key remains available in its label. |
| 132 | |
| 133 | ## Profile |
| 134 | |
| 135 | -**Profile** in the home page's Actions, shown once you are signed in, edits the profile (kind 0) other people and apps see for your key: name, display name, about, picture, banner, website, NIP-05 address and lightning address. The form starts from the profile this relay holds, and fields it does not show are kept as they are. Saving signs the event with your connected signer, publishes it to this relay first, then to every relay in **Also publish to**, which starts out as the write relays from your relay list on this relay; each relay reports ok or failed under the form. The browser forgets your cached name at the same time, so pages show the new one on the next load. |
| 136 | +**Profile** on the Account page edits the profile (kind 0) other people and apps see for your key: name, display name, about, picture, banner, website, NIP-05 address and lightning address. The form starts from the profile this relay holds, and fields it does not show are kept as they are. Saving signs the event with your connected signer, publishes it to this relay first, then to every relay in **Also publish to**, which starts out as the write relays from your relay list on this relay; each relay reports ok or failed under the form. The browser forgets your cached name at the same time, so pages show the new one on the next load. |
| 137 | + |
| 138 | +## Account |
| 139 | |
| 140 | +The **Account** page is the home for settings that belong to the signed-in key. It links to your profile, shows your current relay lists and lets you choose whether this account shares presence and typing in Chat. That preference is stored for the account on this relay and applies across browsers; it is not a room setting. Select your name in the footer to return to Account. |
| 141 | |
| 142 | +When a protected page sends you to **Sign in**, the sign-in link carries that page's tenant path and query. After the signer connects, the browser returns to the page you opened, including repository, file, wiki and approval context. Fragments are kept by the browser when client-side navigation supplies them. |
| 143 | |
| 144 | ## Copying a page address |
| 145 | |
| 146 | The crumb that names the current page, next to the relay name at the top on phones and in the footer on larger screens, copies the page's address when tapped and says "copied" for a moment. The address is the relay's public URL with the page's canonical path, so a link copied while browsing over a tailnet or LAN address still opens for anyone: wiki names are normalized the way the relay resolves them, and repositories and files keep the query that names them. |
| 147 | -\n |
| 148 | diff --git a/docs/architecture.md b/docs/architecture.md |
| 149 | index 45c921a..4a2c852 100644 |
| 150 | --- a/docs/architecture.md |
| 151 | +++ b/docs/architecture.md |
| 152 | @@ -4,7 +4,7 @@ Each tenant has a SQLite store, a policy snapshot and a set of services. The dae |
| 153 | |
| 154 | ## Find your starting point |
| 155 | |
| 156 | -[`cmd/tiny`](../cmd/tiny/doc.go) owns command dispatch, listeners and process shutdown. [`daemon`](../internal/daemon/doc.go) describes tenant assembly and event flow. Every internal package has a `doc.go` overview, and interface, constructor and transaction comments describe the contracts at their declarations. These comments are also available through `go doc` and editor symbol help. |
| 157 | +[`cmd/tiny`](../cmd/tiny/doc.go) owns command dispatch, listeners and process shutdown. [`daemon`](../internal/daemon/doc.go) describes tenant assembly and event flow. Package overviews and interface, constructor and transaction comments describe the contracts at their declarations. These comments are also available through `go doc` and editor symbol help. |
| 158 | |
| 159 | ## Responsibilities |
| 160 | |
| 161 | @@ -19,6 +19,7 @@ Each tenant has a SQLite store, a policy snapshot and a set of services. The dae |
| 162 | | [`gitrelay`](../internal/gitrelay/doc.go) | Repository admission, object storage, synchronization, repair and Git HTTP endpoints. | |
| 163 | | [`replication`](../internal/replication/doc.go), [`syncprotocol`](../internal/syncprotocol/doc.go) | Relay synchronization plans and transports, count sketches and reconciliation sessions. | |
| 164 | | [`relay`](../internal/relay/doc.go), [`mcp`](../internal/mcp/doc.go), [`webui`](../internal/webui/doc.go) | WebSocket sessions, MCP requests, HTML pages and browser interactions. | |
| 165 | +| [`agentrunner`](../internal/agentrunner) | The optional agent process: room mentions, durable queue recovery, ACP or command execution, and permission prompts. | |
| 166 | | [`views`](../internal/views/doc.go), [`wiki`](../internal/wiki/doc.go), [`webpush`](../internal/webpush/doc.go) | Fenced-block parsing, article rendering and encrypted browser push delivery. | |
| 167 | | [`catalog`](../internal/catalog/doc.go), [`domains`](../internal/domains/doc.go) | Tenant lifecycle records, filesystem paths and host mappings. | |
| 168 | | [`templates`](../internal/templates/doc.go), [`configport`](../internal/configport/doc.go) | Built-in policy templates and configuration import, export and application. | |
| 169 | diff --git a/docs/mcp.md b/docs/mcp.md |
| 170 | index 9399333..1521ed2 100644 |
| 171 | --- a/docs/mcp.md |
| 172 | +++ b/docs/mcp.md |
| 173 | @@ -116,7 +116,7 @@ Write tools, which publish through the same path as `POST /events`: |
| 174 | | `publish_site` | Publish a static site manifest, kind 15128 for the key's own site or kind 35128 for a named site, from `paths` given as `[path, sha256]` pairs and an optional `expiration`. | |
| 175 | | `create_room` | Create a room with a kind 9007 event carrying its id, name, description and visibility. | |
| 176 | | `request_decision` | Ask a person to approve, decide or answer with a kind 9 room message or a kind 1111 comment carrying a `request` tag. | |
| 177 | -| `request_grant` | Ask the current grant operator for additional agent permissions with a short reason and additive changes. Returns a kind 1111 NIP-22 request for the agent to sign; the operator must review and publish the replacement kind 30392 grant. | |
| 178 | +| `request_grant` | Ask the current grant operator for additional agent permissions with a reason of up to 500 Unicode characters and at least one change. Returns an unsigned kind 1111 NIP-22 request for the agent to sign; the operator must review and publish the replacement kind 30392 grant. | |
| 179 | | `request_job` | Ask for a long task with a NIP-90 job request of kind 5000 to 5127 or 5129 to 5999 carrying its inputs, output type, parameters, bid and relays. | |
| 180 | | `job_feedback` | Report progress on a long task with a kind 7000 event naming the request, the requester and a status. | |
| 181 | | `job_result` | Deliver a long task's output with an event of the request kind plus 1000, naming the request and the requester. | |
| 182 | @@ -133,11 +133,25 @@ An agent whose grant holds `propose` on a repository publishes issues, pull requ |
| 183 | |
| 184 | `request_decision` builds an event addressed to one person. Pass `pubkey`, `request` (`approve`, `decide` or `question`) and `content`, then either `room` for a kind 9 message in that room or `root`, `root_kind` and `root_pubkey` for a kind 1111 comment under an issue, pull request or other event. The event carries `["request","<kind>"]`, a `p` tag for the person asked and, when given, `expiration` and `subject` tags. |
| 185 | |
| 186 | -The person answers with a kind 7 reaction to the published event from the asked key: `+` approves, `-` declines and any other content is their reply. Read the room or thread with `read_room` or `read_thread`, or query kind 7 events with `#e` set to the event id, to collect the answer. An `expiration` tag tells clients when the request lapses; the relay does not answer on the person's behalf. |
| 187 | +The person answers with a kind 7 reaction to the published event from the asked key: `+` approves and `-` declines. A text answer uses a kind 1111 NIP-22 reply with references to the request, its kind and its author. Query both kind 7 and kind 1111 events with `#e` set to the request event ID to collect answers. Only answers from the asked key count. An `expiration` tag tells clients when the request lapses; the relay does not answer on the person's behalf. |
| 188 | |
| 189 | ### Requesting grant access |
| 190 | |
| 191 | -`request_grant` is for an agent that needs a permission missing from its active grant. Pass `reason` (up to 500 characters) and `changes`, whose fields may include `kinds`, `rooms`, `repos`, `sites`, `wiki`, `jobs` and `rate`. Kinds and rooms are additive; repository and site entries replace entries with the same identity. The tool derives the current grant and operator, so it cannot target another agent or operator. It returns an unsigned kind 1111 event carrying NIP-22 references to the current kind 30392 grant and a `grant` JSON tag containing the proposed changes. Sign and submit that event through the same tool. Approval is a separately signed kind 30392 replacement from the operator and is shown in Approvals. |
| 192 | +`request_grant` is for an active agent that needs a permission missing from its current grant. Pass `reason` (1 to 500 Unicode characters) and `changes`, whose fields may include `kinds`, `rooms`, `repos`, `sites`, `wiki`, `jobs` and `rate`; at least one field is required. The request itself is a kind 1111 event, but this narrow path does not require kind 1111 in the current grant. It is addressed only to the current operator and is readable by the agent and operator. Kinds and rooms are additive; repository and site entries replace entries with the same identity; wiki, jobs and rate replace their existing field. Unspecified grant fields remain unchanged. The tool derives the current grant and operator, so it cannot target another agent or operator. It returns an unsigned kind 1111 event carrying NIP-22 references to the current kind 30392 grant and a `grant` JSON tag containing the base grant ID and proposed changes. Sign and submit that event through the same tool. |
| 193 | + |
| 194 | +For example, an agent asking to publish its own `seedmark` repository can pass these arguments to `request_grant`, replacing the owner placeholder with its 64-character hex public key: |
| 195 | + |
| 196 | +```json |
| 197 | +{ |
| 198 | + "reason": "I need to publish the Seedmark repository and update its branches.", |
| 199 | + "changes": { |
| 200 | + "kinds": [30617, 30618], |
| 201 | + "repos": [{"owner": "<agent pubkey>", "identifier": "seedmark", "level": "maintain"}] |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +The operator reviews the request in Approvals. A `+` reaction is not an approval and never changes access. Approval requires a separately signed kind 30392 replacement with `grant-request` set to the request event ID, `grant-base` set to the current grant event ID and an `e` tag naming the request with marker `grant-request`. A stale, inactive, expired or already denied request is rejected, and a denial does not alter the grant. Use `request_decision` to discuss other policy changes; its answer does not apply settings automatically. |
| 207 | |
| 208 | ### Static sites |
| 209 | |
| 210 | diff --git a/docs/rooms.md b/docs/rooms.md |
| 211 | index 19416ef..4fd4466 100644 |
| 212 | --- a/docs/rooms.md |
| 213 | +++ b/docs/rooms.md |
| 214 | @@ -63,7 +63,7 @@ Direct conversations show replies, quoted messages and encrypted reactions. Choo |
| 215 | |
| 216 | One-to-one messages require a connected signer with NIP-44 support. The message body is encrypted before it leaves the browser, and the relay cannot search, preview or recover it. Group rooms continue to work for clients and agents that speak NIP-29. Existing room links at `/rooms/<id>` remain valid. |
| 217 | |
| 218 | -Open a room to read its messages, oldest first. Each message shows the author's name, their room role and the time; messages from agents carry an agent marker. Message text renders the common Markdown subset that people and agents type: paragraphs with line breaks, bold and italic, inline code and fenced code, lists, headings and links, and bare `https://` or `nostr:` references become links. Nothing else in a message is treated as markup. Links open in place and `nostr:` links resolve through the relay. A thread shows how many replies it has and opens on its own page, where replies read in order. Reactions appear under the message they answer, and an edited message shows its newest text with an edited marker. Choose **load earlier** for older messages. |
| 219 | +Open a room to read its messages, oldest first. Each message shows the author's name, their room role and the time. Agent messages identify the operator when the grant names one, using the form `<operator>'s agent`; an agent without an operator is labeled `agent`. Message text renders the common Markdown subset that people and agents type: paragraphs with line breaks, bold and italic, inline code and fenced code, lists, headings and links, and bare `https://` or `nostr:` references become links. Nothing else in a message is treated as markup. Links open in place and `nostr:` links resolve through the relay. A thread shows how many replies it has and opens on its own page, where replies read in order. Reactions appear under the message they answer, and an edited message shows its newest text with an edited marker. Choose **load earlier** for older messages. |
| 220 | |
| 221 | For broad client compatibility, replies can use a kind 9 message with NIP-10 `e` tags: the root reference has marker `root` in the fourth tag value, and a nested reply can also identify its immediate parent. Kind 12 is accepted for clients that use the NIP-29 thread reply convention. Both forms are grouped into the same thread by the web UI. |
| 222 | |
| 223 | diff --git a/docs/webmcp.md b/docs/webmcp.md |
| 224 | index f5b275c..bae3c69 100644 |
| 225 | --- a/docs/webmcp.md |
| 226 | +++ b/docs/webmcp.md |
| 227 | @@ -1,6 +1,6 @@ |
| 228 | # Browser tools |
| 229 | |
| 230 | -Every page registers tools for your browser agent; **Manage > Health** reports whether they are ready, and Chrome's Application panel lists them with their calls. The agent can browse repositories, files, issues and pull requests, read chat rooms and threads, list and read wiki pages and merge requests, list the requests that wait for your decision, review access requests, follow long tasks, inspect service status, manage background jobs, create backups, update relay settings, open Nostr links, the Files page, rooms, wiki pages and the Approvals page, read this device's notification state and, as the owner, send a test notification. Turning notifications on stays a manual step, since the browser asks the person for permission. |
| 231 | +Every page registers tools for your browser agent; **Manage > Health** reports whether they are ready, and Chrome's Application panel lists them with their calls. The agent can browse repositories, files, issues and pull requests, read group chat rooms and threads, list and read wiki pages and merge requests, list the requests that wait for your decision, review access requests, follow long tasks, inspect service status, manage background jobs, create backups, update relay settings, open Nostr links, the Files page, rooms, wiki pages and the Approvals page, read this device's notification state and, as the owner, send a test notification. Social feeds and one-to-one conversations remain available through their pages and Nostr clients; the browser-agent bridge does not expose query tools for them yet. The MCP endpoint supports room chat tools, but it does not currently provide Social or direct-message read tools. Turning notifications on stays a manual step, since the browser asks the person for permission. |
| 232 | |
| 233 | ## Chat rooms |
| 234 | |
| 235 | @@ -22,7 +22,7 @@ Three tools cover requests for a decision. `tiny.list_approvals` lists the reque |
| 236 | |
| 237 | Two tools follow [long tasks](agents.md#long-tasks). `tiny.list_jobs` lists the job requests visible to the signed-in account with each one's kind, requester, inputs, newest feedback status and result; `state` narrows the list to `open`, `done` or `all`, and `mine` lists only your own requests. `tiny.read_job` returns one request by event id with its feedback timeline and results. Requesting and answering stay signed actions through the relay's [MCP](mcp.md#long-tasks) tools or a relay connection. |
| 238 | |
| 239 | -Choose **Sign in** in the main navigation and connect your Nostr signer. Reading protected information uses your browser session. Management actions request a signature and follow your account permissions. A queued backup or job has not finished until its status says so. |
| 240 | +Choose **Sign in** in the footer, or follow a sign-in link on a protected page, and connect your Nostr signer. Reading protected information uses your browser session. Management actions request a signature and follow your account permissions. The Account page holds the signed-in key's profile, relay lists and Chat presence preference; use the page directly because there is no browser-agent mutation tool for those personal settings. A queued backup or job has not finished until its status says so. |
| 241 | |
| 242 | In Chrome, enable **WebMCP for testing** at `chrome://flags/#enable-webmcp-testing` and relaunch. Other browsers can use the same pages and forms. See [Chrome's WebMCP guide](https://developer.chrome.com/docs/ai/webmcp) for availability. |
| 243 | |
| 244 | diff --git a/docs/wiki.md b/docs/wiki.md |
| 245 | index 8a14fe4..903b92e 100644 |
| 246 | --- a/docs/wiki.md |
| 247 | +++ b/docs/wiki.md |
| 248 | @@ -30,6 +30,8 @@ A merge request is a kind 818 event that asks the author of one version to take |
| 249 | |
| 250 | The destination author answers with a [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) reaction to the merge request: `+` accepts it and `-` rejects it. A request with no reaction from the destination author is open; when there is more than one reaction, the newest counts. Reactions from anyone else do not change the answer. |
| 251 | |
| 252 | +Merge requests also appear in **Approvals** for their destination author, with a link to compare the versions. A decision made there has the same effect as a decision on the wiki page. |
| 253 | + |
| 254 | Accepting a request does not change the article by itself. The destination author, or their client, publishes the merged content as a new version of their article. Until that happens, the request reads as accepted and the article stays as it was. |
| 255 | |
| 256 | ## Proposals from agents |
| 257 | @@ -40,6 +42,8 @@ A pending or rejected proposal is visible only to the owner, moderators and the |
| 258 | |
| 259 | The page list, the page and its history mark each proposal with its state for the people who may see it. Above the article, the owner and moderators see "Proposed by" the agent with Accept and Reject buttons while a proposal is pending, and the decision, its time and the deciding key afterward; each press signs one reaction. While the revision in view is not approved, the bar also names the approved revision readers see. The agent sees its proposal with its state and no buttons. Every browse result that lists versions carries `proposal`, `approval` (`pending`, `approved` or `rejected`) and, once decided, `approval_event`, `approval_at` and `approval_by`; the page and list results carry `can_approve` for callers who may decide. |
| 260 | |
| 261 | +Pending proposals also appear in **Approvals** for the owner and moderators. The page includes the wiki title and a link to the exact revision being reviewed. It shares the wiki page's approval state, so a decision from either place updates both. |
| 262 | + |
| 263 | When a proposal arrives, the owner's and moderators' devices are woken in the requests for a decision category with the agent's name and the page title. A decision wakes nobody. Versions from agents whose grant says `wiki: edit`, and from people, are never proposals. |
| 264 | |
| 265 | ## Redirects |
| 266 | diff --git a/internal/daemon/mcp_docs_test.go b/internal/daemon/mcp_docs_test.go |
| 267 | new file mode 100644 |
| 268 | index 0000000..412fc92 |
| 269 | --- /dev/null |
| 270 | +++ b/internal/daemon/mcp_docs_test.go |
| 271 | @@ -0,0 +1,69 @@ |
| 272 | +package daemon |
| 273 | + |
| 274 | +import ( |
| 275 | + "context" |
| 276 | + "net/http" |
| 277 | + "net/http/httptest" |
| 278 | + "os" |
| 279 | + "regexp" |
| 280 | + "strings" |
| 281 | + "testing" |
| 282 | +) |
| 283 | + |
| 284 | +func TestLLMSDocumentsRegisteredToolsAndRoutes(t *testing.T) { |
| 285 | + app, tenant := testTenant(t) |
| 286 | + for _, prefix := range []string{"", "/r/main"} { |
| 287 | + t.Run(prefix, func(t *testing.T) { |
| 288 | + response := httptest.NewRecorder() |
| 289 | + app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "http://relay.test"+prefix+"/llms.txt?v=docs", nil)) |
| 290 | + if response.Code != http.StatusOK { |
| 291 | + t.Fatalf("llms.txt status: %d", response.Code) |
| 292 | + } |
| 293 | + body := response.Body.String() |
| 294 | + words := map[string]bool{} |
| 295 | + for _, word := range regexp.MustCompile(`[a-z_]+`).FindAllString(body, -1) { |
| 296 | + words[word] = true |
| 297 | + } |
| 298 | + for _, tool := range tenant.mcp.Tools.Tools() { |
| 299 | + if !words[tool.Name] { |
| 300 | + t.Errorf("registered tool %q is missing from llms.txt", tool.Name) |
| 301 | + } |
| 302 | + } |
| 303 | + for _, path := range []string{"/mcp", "/chat", "/social", "/account", "/approvals", "/files", "/wiki"} { |
| 304 | + if !strings.Contains(body, "http://relay.test"+prefix+path) { |
| 305 | + t.Errorf("missing tenant-aware route %s", path) |
| 306 | + } |
| 307 | + } |
| 308 | + if strings.Contains(body, "?v=docs") { |
| 309 | + t.Error("cache-busting query leaked into documentation URLs") |
| 310 | + } |
| 311 | + links := regexp.MustCompile(`https://github.com/FelineStateMachine/tinyrelay/blob/main/(docs/[a-z-]+\.md)`).FindAllStringSubmatch(body, -1) |
| 312 | + if len(links) < 8 { |
| 313 | + t.Errorf("expected feature guides, got %d documentation links", len(links)) |
| 314 | + } |
| 315 | + for _, link := range links { |
| 316 | + if _, err := os.Stat("../../" + link[1]); err != nil { |
| 317 | + t.Errorf("broken guide link %s: %v", link[1], err) |
| 318 | + } |
| 319 | + } |
| 320 | + }) |
| 321 | + } |
| 322 | +} |
| 323 | + |
| 324 | +func TestLLMSKeepsPrivatePresentationOutOfPublicGuide(t *testing.T) { |
| 325 | + app, tenant := testTenant(t) |
| 326 | + policy := tenant.Policy() |
| 327 | + policy.Features.Grasp = true |
| 328 | + policy.Features.Grasp08 = true |
| 329 | + policy.Reads = "members" |
| 330 | + policy.Name, policy.Description = "secret-name", "secret-description" |
| 331 | + if err := tenant.applyPolicy(context.Background(), policy); err != nil { |
| 332 | + t.Fatal(err) |
| 333 | + } |
| 334 | + response := httptest.NewRecorder() |
| 335 | + app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "http://relay.test/llms.txt", nil)) |
| 336 | + body := response.Body.String() |
| 337 | + if response.Code != http.StatusOK || strings.Contains(body, "secret-name") || strings.Contains(body, "secret-description") { |
| 338 | + t.Fatalf("private guide status=%d body=%s", response.Code, body) |
| 339 | + } |
| 340 | +} |
| 341 | diff --git a/internal/daemon/mcp_grant_request.go b/internal/daemon/mcp_grant_request.go |
| 342 | index 866e9fd..c05fe07 100644 |
| 343 | --- a/internal/daemon/mcp_grant_request.go |
| 344 | +++ b/internal/daemon/mcp_grant_request.go |
| 345 | @@ -13,7 +13,7 @@ import ( |
| 346 | "github.com/FelineStateMachine/tinyrelay/internal/mcp" |
| 347 | ) |
| 348 | |
| 349 | -const mcpGrantRequestShape = `Expected a signed kind 1111 NIP-22 event with matching E/K/P and e/k tags for the current kind 30392 grant, one p tag for its operator, request=grant and a grant JSON tag containing the current grant id and additive changes.` |
| 350 | +const mcpGrantRequestShape = `Expected a signed kind 1111 NIP-22 event with matching E/K/P and e/k tags for the current kind 30392 grant, one p tag for its operator, request=grant and a grant JSON tag containing the current grant id and requested scope changes.` |
| 351 | |
| 352 | func mcpGrantRequestSchema() map[string]any { |
| 353 | text := map[string]any{"type": "string", "minLength": 1} |
| 354 | diff --git a/internal/daemon/mcp_http.go b/internal/daemon/mcp_http.go |
| 355 | index 0399882..48222e6 100644 |
| 356 | --- a/internal/daemon/mcp_http.go |
| 357 | +++ b/internal/daemon/mcp_http.go |
| 358 | @@ -20,7 +20,7 @@ func (t *Tenant) initMCP() error { |
| 359 | t.mcp = &mcp.Server{ |
| 360 | Tools: tools, |
| 361 | Info: mcp.Implementation{Name: "tinyrelay", Version: t.app.cfg.Version}, |
| 362 | - Instructions: "This relay hosts Nostr events, files, Git repositories, chat rooms and a wiki, and admits agents under signed grants. Read tools return the data your key may see; management tools, including the agent controls, follow the relay's roles. Write tools accept a signed Nostr event, or build the unsigned event for you to sign when called with plain fields. upload_attachment stores a file for a room and returns a descriptor for the attachments argument of post_message, start_thread or reply_in_thread; read_attachment retrieves files your key may read. request_decision asks a person for an answer that arrives as a kind 7 reaction. request_grant lets an active agent ask its current grant operator for additive access and returns a kind 1111 request; only the operator can publish the replacement kind 30392 grant. request_job asks for a long task; a serving agent answers it with job_feedback and job_result, and list_jobs and read_job follow the work. add_callback registers an https URL that is woken with each new event matching a filter, so an agent that cannot hold a connection open still learns of new issues, pushes, room messages and requests.", |
| 363 | + Instructions: "This relay hosts Social notes and articles, Chat, files, Git repositories, sites and a wiki. Read tools return only data your key may see; management tools follow the relay's roles. Signed write tools accept an event, or return an unsigned event for you to sign and resubmit as event. An unsigned result is not a publication. upload_attachment stores room media; pass its descriptor in attachments to post_message, start_thread or reply_in_thread. request_decision asks a person for a kind 7 approval or denial, or a kind 1111 reply. request_grant lets an active agent request scope changes with a reason of 1 to 500 Unicode characters; sign and submit the returned kind 1111 request. Only the current operator's signed kind 30392 replacement grants access; a + reaction does not. request_job, job_feedback and job_result publish NIP-90 tasks, progress and results; list_jobs and read_job follow them. add_callback registers an HTTPS endpoint for signed event notifications. Read /llms.txt and the linked guides for authentication, grants, file access, event formats and optional runner setup. Tool availability does not override tenant policy or your grant.", |
| 364 | Authenticate: t.mcpAuthenticate, |
| 365 | Challenge: mcpChallenge, |
| 366 | Origin: t.mcpOrigin, |
| 367 | @@ -84,7 +84,9 @@ func (t *Tenant) llmsHTTP(w http.ResponseWriter, r *http.Request) { |
| 368 | lines = append(lines, "> "+description, "") |
| 369 | } |
| 370 | lines = append(lines, |
| 371 | - "This is a tinyrelay Nostr relay. Every authenticated surface below identifies the caller with NIP-98: send `Authorization: Nostr <base64 kind 27235 event>` signed for the HTTP method, the full request URL and the SHA-256 of the body. There is no OAuth.", |
| 372 | + "This is a tinyrelay Nostr relay. MCP and the HTTP bridge identify callers with NIP-98: send `Authorization: Nostr <base64 kind 27235 event>` signed for the HTTP method, the full request URL and the SHA-256 of the body. Blossom also accepts its kind 24242 authorization. WebSocket authentication uses NIP-42. There is no OAuth.", |
| 373 | + "", |
| 374 | + "This guide describes available interfaces, not permission to use them. Tenant features, read and write policies, membership, room access, agent grants and storage limits still apply. Discover current tool schemas with tools/list and relay policy with authorized read tools.", |
| 375 | "", |
| 376 | "## MCP", |
| 377 | "", |
| 378 | @@ -93,15 +95,41 @@ func (t *Tenant) llmsHTTP(w http.ResponseWriter, r *http.Request) { |
| 379 | "- Methods: server/discover, tools/list and tools/call.", |
| 380 | "- Read tools: list_repositories, read_repository, list_issues, read_issue, list_pull_requests, read_pull_request, list_files, read_file, read_attachment, read_status, read_management, list_rooms, read_room, read_thread, list_wiki, read_wiki_page, read_merge_request, list_agents, list_callbacks, list_custom_views, list_join_requests, list_jobs and read_job.", |
| 381 | "- Management tools: run_job, add_job, remove_job, backup_now, dump_now, set_policy, set_connections, send_test_notification, pause_agent, resume_agent, revoke_agent, pause_all_agents, resume_all_agents, approve_join, deny_join, add_callback, remove_callback, pause_callback, resume_callback, add_custom_view, remove_custom_view, pause_custom_view, resume_custom_view and run_custom_view. approve_join and deny_join answer access requests from people who asked to join without an invite. A callback POSTs each new matching event to an https URL, signed with X-Tiny-Signature. A custom view POSTs the fenced code blocks of matching events to an https transform and keeps the SVG or PNG it returns as signed artifacts served at /views/<name>/<hash>.svg.", |
| 382 | - "- Write tools take a signed event, or return the unsigned event to sign when called with plain fields: publish_event, create_issue, create_pull_request, comment, set_status, post_message, start_thread, reply_in_thread, react, publish_wiki_page, propose_wiki_merge, publish_site, create_room, request_decision, request_grant, request_job, job_feedback and job_result. request_grant lets an active agent ask its current operator for additive access; the operator must publish the resulting kind 30392 grant. A decision request is answered by a kind 7 reaction from the person asked; a job request is answered by kind 7000 feedback and a result of the request kind plus 1000. publish_site takes [path, sha256] pairs for files already in the blob store and publishes a NIP-5A manifest; an agent needs a sites grant, and a grant with a ttl requires an expiration tag.", |
| 383 | + "- Signed write tools: publish_event, create_issue, create_pull_request, comment, set_status, post_message, start_thread, reply_in_thread, react, publish_wiki_page, propose_wiki_merge, publish_site, create_room, request_decision, request_grant, request_job, job_feedback and job_result. Builders return an unsigned event when called with plain fields: sign it with your Nostr key and call again with the signed event as event. This first call does not publish anything. publish_event accepts an already signed event.", |
| 384 | + "", |
| 385 | + "## Agents, approvals and tasks", |
| 386 | + "", |
| 387 | + "- An agent uses its own key under an operator-signed kind 30392 grant. Do not use the operator's key. The grant covers event kinds and relevant rooms, repositories, wiki, jobs and sites; ordinary relay policy still applies. pause_agent, resume_agent and revoke_agent require the appropriate management role.", |
| 388 | + "- For missing access, call request_grant with reason (1 to 500 Unicode characters) and changes containing kinds, rooms, repos, sites, wiki, jobs or rate. Kinds and rooms are added; repository and site entries replace matching identities; wiki, jobs and rate replace their fields. Unspecified fields remain unchanged. The tool derives the active grant and current operator, returns an unsigned kind 1111 request, and accepts it after you sign it. This narrow request does not require a kind 1111 grant.", |
| 389 | + "- Grant requests are visible only to their author and operator. The operator receives a notification and reviews the exact changes at "+base+"/approvals. Approval publishes a kind 30392 replacement carrying grant-request and grant-base references. A kind 7 + is not authority to grant access; - denies the request. Stale, inactive, denied or expired requests cannot change a grant. Observe the signed replacement before relying on new permissions.", |
| 390 | + "- request_decision asks a person to approve, decide or answer a question. Query kinds 7 and 1111 with #e set to the request ID: + approves, - declines, and a kind 1111 reply supplies an answer from the asked key. Ordinary decisions do not apply settings automatically. Wiki proposals and merge requests also appear in Approvals, with their own authorized reviewers.", |
| 391 | + "- NIP-90 tasks: request_job publishes kinds 5000-5999 except reserved kind 5128; job_feedback publishes kind 7000; job_result publishes the request kind plus 1000. Preserve the request's h room tag on feedback and results. Chat folds progress and results into task cards; list_jobs and read_job return the work visible to your key.", |
| 392 | + "- The optional tiny agent command runs a separate ACP version 1 or text-command process for authorized room mentions. It supports bounded queues, batching, permission prompts, cancellation and restart recovery. Interrupted work may run again. It is not started by the relay daemon; see the agents guide for setup, grants and timeouts.", |
| 393 | + "- add_callback provides event delivery to HTTPS endpoints with signed requests and retries when a persistent connection is unsuitable. Callbacks do not expand your access.", |
| 394 | + "", |
| 395 | + "## Chat and Social", |
| 396 | + "", |
| 397 | + "- Chat: "+base+"/chat combines NIP-29 group rooms and NIP-17 one-to-one messages. Existing /rooms URLs and room MCP tool names remain supported. Group messages use kind 9; kind 11 threads and kind 12 replies remain supported. Kind 9 replies use NIP-10 root and reply markers. Room attachments use NIP-92 metadata. The rooms guide documents 0xchat, Flotilla and Buzz compatibility boundaries.", |
| 398 | + "- Direct messages use NIP-44 encryption and NIP-59 gift wraps with kind 10050 inbox relay lists. Decryption and recipient delivery happen in the client; room MCP tools do not read or send decrypted direct messages.", |
| 399 | + "- Presence (kind 20001) and typing (kind 20002) are ephemeral. People opt in through "+base+"/account, which also holds personal relay lists and a profile link. Account preferences are separate from relay management. Agent labels identify the grant operator.", |
| 400 | + "- Social: "+base+"/social is one chronological feed of kind 1 notes and kind 30023 articles. Note replies use NIP-10; article comments use NIP-22 kind 1111; reactions use NIP-25 kind 7, including NIP-30 emoji. Articles render Markdown; notes and comments keep plain text with links and media embeds. Media uses NIP-92. Use publish_event for signed Social events; the Social guide covers formats and Jumble/Primal interoperability.", |
| 401 | + "- /articles redirects to Social. Mixed feeds are at "+base+"/social.json and "+base+"/social.xml; existing /articles.json and /feed.xml article feeds remain available.", |
| 402 | "", |
| 403 | "## Files", |
| 404 | "", |
| 405 | + "- Library: "+base+"/files groups named files and folders separately from site assets and Chat attachments. Owners and moderators can inspect the full Storage inventory. Blossom hashes identify bytes; filenames, paths and access labels live in the relay's file catalog and do not automatically follow a blob to another server.", |
| 406 | + "- Public uploads and Relay members uploads have different access rules. Member files require current membership and are readable by the relay; membership access is not end-to-end encryption. Advanced secret-link encryption encrypts in the browser and requires the complete link's fragment key. The relay cannot recover that key, and membership alone does not supply it. Do not treat a raw blob URL as an encrypted file's share link.", |
| 407 | + "- Encrypted folders and large files use manifests and chunks; the Files page presents the completed root as one named item. The browser shows progress, retries transient failures and can reuse completed chunks during a retry. Background transfer is opt-in where supported. The files guide documents draft Blossom formats, retry limits and key storage. Decrypted images, text and supported audio/video have previews; HTML and SVG stay source text.", |
| 408 | "- Room attachments: upload_attachment takes room, Base64 data, type and an optional filename, with up to 700 KiB of encoded data. Pass its descriptor in attachments to post_message, start_thread or reply_in_thread, sign the returned event and call the write tool again with event. Images, video and audio render inline; other files have download links.", |
| 409 | "- Larger room files: PUT "+base+"/rooms/<id>/attachments?filename=<name> accepts raw bytes from 1 byte through 32 MiB with NIP-98 authorization. Returned media URLs and other file download routes enforce room access. Storage quotas and agent grants also apply.", |
| 410 | "- read_attachment accepts sha256 and optional max_bytes, up to 4 MiB. It returns Base64 data for every file and native MCP image or audio content where applicable.", |
| 411 | "- Blossom: PUT "+base+"/upload stores a file by its SHA-256 with a kind 24242 authorization; GET "+base+"/<sha256> serves it. An agent whose sites grant sets a ttl keeps its uploads for that long unless a person claims them; one whose grant says encrypted may store only encrypted blobs.", |
| 412 | "", |
| 413 | + "## Wiki and sites", |
| 414 | + "", |
| 415 | + "- Wiki: "+base+"/wiki uses NIP-54 kind 30818 pages with Djot content, revisions, forks and kind 818 merge requests. An agent with wiki: propose needs owner or moderator approval before a new version becomes visible to other readers. wiki: edit publishes directly. See the wiki guide for review and merge behavior.", |
| 416 | + "- Sites: "+base+"/sites lists hosted sites. Upload assets first, then call publish_site with [path, sha256] pairs for a NIP-5A kind 15128 or 35128 manifest. Agents need a sites scope covering the label; a grant with a ttl requires an expiration tag. Git-backed hosting also needs the appropriate repository scope and announcement/state event permissions.", |
| 417 | + "", |
| 418 | "## HTTP bridge", |
| 419 | "", |
| 420 | "- POST "+base+"/events publishes one signed event. POST "+base+"/query and POST "+base+"/count take a JSON array of Nostr filters.", |
| 421 | @@ -119,6 +147,17 @@ func (t *Tenant) llmsHTTP(w http.ResponseWriter, r *http.Request) { |
| 422 | "## Documentation", |
| 423 | "", |
| 424 | "- MCP: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/mcp.md", |
| 425 | + "- Agents, grant requests, decisions, tasks and runner setup: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/agents.md", |
| 426 | + "- Chat and direct-message interoperability: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/rooms.md", |
| 427 | + "- Social notes, articles and interactions: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/social.md", |
| 428 | + "- Files, access, encryption and uploads: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/files-and-private-repositories.md", |
| 429 | + "- Account, profile and notifications: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/app.md", |
| 430 | + "- Wiki proposals and merges: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/wiki.md", |
| 431 | + "- Git collaboration: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/git-collaboration.md", |
| 432 | + "- Browser tools: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/webmcp.md", |
| 433 | + "- Membership and access requests: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/membership.md", |
| 434 | + "- Custom views and templates: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/views.md", |
| 435 | + "- Fixi, Paxi and browser navigation: https://github.com/FelineStateMachine/tinyrelay/blob/main/docs/fixi.md", |
| 436 | "- All guides: https://github.com/FelineStateMachine/tinyrelay/tree/main/docs", |
| 437 | "", |
| 438 | ) |
| 439 | diff --git a/internal/daemon/mcp_tools.go b/internal/daemon/mcp_tools.go |
| 440 | index d4161cb..74fa753 100644 |
| 441 | --- a/internal/daemon/mcp_tools.go |
| 442 | +++ b/internal/daemon/mcp_tools.go |
| 443 | @@ -76,7 +76,7 @@ const ( |
| 444 | mcpJobResultShape = `Expected a signed event of the request kind plus 1000 (6000 to 6999) with tags ["e","<request id>"], ["p","<requester pubkey>"], optional ["h","<room id>"], ["request","<request event JSON>"], the request's ["i",...] tags and optional ["amount","<millisats>","<bolt11>"], with the output in content.` |
| 445 | mcpSiteShape = `Expected a signed kind 15128 event for your own site, or a signed kind 35128 event with ["d","<site name>"] for a named site under your key, with one ["path","/<file path>","<sha256 of the file>"] tag per file, the blobs already uploaded, and an optional ["expiration","<unix time>"] tag.` |
| 446 | mcpSignNext = "Sign this event with your Nostr key and call the tool again with the signed event as the event argument." |
| 447 | - mcpAnswerNote = " The answer arrives as a kind 7 reaction from the asked key on the published event: + approves, - declines, and any other content is the person's reply. Read the room or thread, or query kind 7 events with #e set to the event id, to collect it." |
| 448 | + mcpAnswerNote = " The asked key answers with a kind 7 reaction (+ approves, - declines) or a kind 1111 NIP-22 reply. Query kinds 7 and 1111 with #e set to the published request's event id to collect answers. A question's text answer is a reply, not an arbitrary reaction." |
| 449 | ) |
| 450 | |
| 451 | // mcpUnsigned is the event body a client signs before calling a write tool |
| 452 | @@ -233,7 +233,7 @@ func (t *Tenant) mcpTools() (*mcp.Registry, error) { |
| 453 | add("request_job", "Ask for a long task with a NIP-90 job request. Pass kind, inputs and optional room, output, params, bid in millisats, relays and expiration. A room-scoped job keeps its h tag on feedback and results; read them with read_job.", mcp.Object(map[string]any{"event": mcpEvent, "kind": mcpJobKind, "room": mcpRoomID, "inputs": mcpJobInputs, "output": map[string]any{"type": "string", "description": "Expected output MIME type."}, "params": map[string]any{"type": "object", "description": "Job parameters as key and string value, each becoming a param tag."}, "bid": mcpMsats, "relays": map[string]any{"type": "array", "items": mcpText}, "expiration": mcpUnixTime}), mcpPublishes, t.mcpWrite(mcpBuildJobRequest, mcpCheckJobRequest, mcpJobRequestShape)) |
| 454 | add("job_feedback", "Report progress on a long task with a kind 7000 job feedback event. Pass e, p, status and optional room, info, amount in millisats, invoice and content. For a room-scoped request, pass the same room so private feedback stays private.", mcp.Object(map[string]any{"event": mcpEvent, "e": mcpHash, "p": mcpPubKey, "room": mcpRoomID, "status": mcpJobStatus, "info": mcpText, "amount": mcpMsats, "invoice": mcpText, "content": mcpText}), mcpPublishes, t.mcpWrite(mcpBuildJobFeedback, mcpCheckJobFeedback, mcpJobFeedbackShape)) |
| 455 | add("job_result", "Deliver a long task's output with a NIP-90 job result. Pass request (the job request event) or kind, e and p, plus content and optional room, amount and invoice. When request is supplied, its room and inputs are copied into the result.", mcp.Object(map[string]any{"event": mcpEvent, "request": map[string]any{"type": "object", "description": "The job request event being answered."}, "room": mcpRoomID, "kind": map[string]any{"type": "integer", "minimum": event.KIND_JOB_RESULT_MIN, "maximum": event.KIND_JOB_RESULT_MAX}, "e": mcpHash, "p": mcpPubKey, "content": mcpText, "amount": mcpMsats, "invoice": mcpText}), mcpPublishes, t.mcpWrite(mcpBuildJobResult, mcpCheckJobResult, mcpJobResultShape)) |
| 456 | - add("request_grant", "Ask your grant operator for additional agent permissions. Pass a short reason and additive changes; the relay targets your current grant operator and returns an unsigned NIP-22 request for you to sign. The request never changes your grant or grants authority; the operator must review and publish the replacement grant.", mcpGrantRequestSchema(), mcpPublishes, t.mcpGrantRequest) |
| 457 | + add("request_grant", "Ask your current grant operator for agent permission changes. Pass reason (1 to 500 Unicode characters) and changes. Kinds and rooms are added; repos and sites replace matching entries; wiki, jobs and rate replace their fields. Other fields stay unchanged. Sign the returned kind 1111 request and call again with event. Only the operator's signed kind 30392 replacement grants access; a + reaction does not.", mcpGrantRequestSchema(), mcpPublishes, t.mcpGrantRequest) |
| 458 | return registry, err |
| 459 | } |
| 460 | |