| 1 | diff --git a/docs/agents.md b/docs/agents.md |
| 2 | index 3ae3da3..afad341 100644 |
| 3 | --- a/docs/agents.md |
| 4 | +++ b/docs/agents.md |
| 5 | @@ -19,6 +19,7 @@ The owner or a moderator grants an agent by publishing a kind 30392 event addres |
| 6 | | `k` | No | An event kind the agent may publish. Repeat for each kind. | |
| 7 | | `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](wiki.md#proposals-from-agents). | |
| 8 | | `jobs` | No | `request`, `serve` or `both`. Lets the agent publish long task requests, answer them, or both. See [Long tasks](#long-tasks). | |
| 9 | +| `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](#static-sites). | |
| 10 | | `rate` | No | Events per minute, 1 to 600. The default is 60. | |
| 11 | |
| 12 | The content may be empty or a JSON note for your own records. |
| 13 | @@ -53,7 +54,8 @@ A key that already has a human role keeps that role. A grant never lowers a memb |
| 14 | 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: |
| 15 | |
| 16 | - The grant is not paused, not revoked and not expired. |
| 17 | -- The event kind appears in the grant's `k` tags, or the grant's `wiki` or `jobs` tag covers it. Profiles (kind 0) and relay lists (kind 10002) are always allowed. |
| 18 | +- 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. |
| 19 | +- 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](#static-sites). |
| 20 | - 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. |
| 21 | - 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. |
| 22 | - If the event carries an `h` tag, the room appears in the grant's `room` tags. |
| 23 | @@ -88,7 +90,7 @@ Either way the agent loses its role at once. A fresh grant restores access. |
| 24 | |
| 25 | ## Manage > Agents |
| 26 | |
| 27 | -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 (`read` or `maintain`), wiki access, 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. |
| 28 | +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 (`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. |
| 29 | |
| 30 | 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. |
| 31 | |
| 32 | @@ -101,7 +103,7 @@ The **New agent** form signs a grant with your connected signer. Give the agent |
| 33 | - **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. |
| 34 | - **Paste a public key** grants an agent that already has a key. |
| 35 | |
| 36 | -Add the rooms and kinds the agent may post in, one repository per line as `<owner pubkey>:<identifier>:read` or `:maintain`, wiki access, 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. |
| 37 | +Add the rooms and kinds the agent may post in, one repository per line as `<owner pubkey>:<identifier>: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. |
| 38 | |
| 39 | ## Asking a person |
| 40 | |
| 41 | @@ -208,6 +210,50 @@ Over MCP, `request_job` builds a request, `job_feedback` and `job_result` build |
| 42 | |
| 43 | 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. |
| 44 | |
| 45 | +## Static sites |
| 46 | + |
| 47 | +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](https://github.com/nostr-protocol/nips/pull/2004) 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. |
| 48 | + |
| 49 | +### The grant |
| 50 | + |
| 51 | +Each `sites` tag names one site the agent may publish: |
| 52 | + |
| 53 | +``` |
| 54 | +["sites", "<label>", "ttl=<days>", "encrypted"] |
| 55 | +``` |
| 56 | + |
| 57 | +| Value | Required | Meaning | |
| 58 | +| --- | --- | --- | |
| 59 | +| `<label>` | Yes | A site label under the agent's key, or `*` for every site under it. A label under another key is refused. | |
| 60 | +| `ttl=<days>` | No | How long the site and its files live, 1 to 365 days. | |
| 61 | +| `encrypted` | No | The agent's uploads must be encrypted. | |
| 62 | + |
| 63 | +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: |
| 64 | + |
| 65 | +```json |
| 66 | +{ |
| 67 | + "kind": 30392, |
| 68 | + "tags": [ |
| 69 | + ["d", "<agent pubkey>"], |
| 70 | + ["p", "<agent pubkey>"], |
| 71 | + ["name", "previews"], |
| 72 | + ["expiration", "1735689600"], |
| 73 | + ["sites", "*", "ttl=7", "encrypted"] |
| 74 | + ], |
| 75 | + "content": "" |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +### What the relay enforces |
| 80 | + |
| 81 | +- 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. |
| 82 | +- 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. |
| 83 | +- 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. |
| 84 | +- 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. |
| 85 | +- 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. |
| 86 | + |
| 87 | +Over MCP, `publish_site` builds the manifest from the uploaded files' paths and hashes. See [MCP](mcp.md#static-sites). |
| 88 | + |
| 89 | ## Callbacks |
| 90 | |
| 91 | 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. |
| 92 | diff --git a/docs/files-and-private-repositories.md b/docs/files-and-private-repositories.md |
| 93 | index 9070c11..6b6efdc 100644 |
| 94 | --- a/docs/files-and-private-repositories.md |
| 95 | +++ b/docs/files-and-private-repositories.md |
| 96 | @@ -51,6 +51,10 @@ This example limits each stored blob to 100 MiB and allows 1 GiB of claimed stor |
| 97 | |
| 98 | Each uploader pays the full size of every file they claim, even when another uploader already stores identical bytes. Repeated uploads by the same user count once. Removing an uploader's claim releases their allowance; shared content remains while another claim exists. An owner can remove the stored file for everyone. |
| 99 | |
| 100 | +## Agent uploads |
| 101 | + |
| 102 | +An agent whose grant carries a `sites` tag with a ttl uploads on a clock: each file it stores is kept for the longest ttl among the grant's site entries, then removed by maintenance. A person who claims the same file keeps it, and the agent's claim alone is released. An agent whose grant says `encrypted` may store only encrypted blobs and manifests. See [Static sites](agents.md#static-sites). |
| 103 | + |
| 104 | Clients can check an upload with [BUD-06](https://github.com/hzrd149/blossom/blob/master/buds/06.md) `HEAD /upload`, including empty files with `X-Content-Length: 0`. The result is advisory; the upload itself checks the current limits. File descriptors include the optional Nostr metadata field defined by [BUD-08](https://github.com/hzrd149/blossom/blob/master/buds/08.md). A [BUD-13](https://github.com/hzrd149/blossom/pull/100) remote upload may name up to eight `url` sources. |
| 105 | |
| 106 | ## Private Git hosting |
| 107 | diff --git a/docs/observability.md b/docs/observability.md |
| 108 | index 6a058c0..27f4ae3 100644 |
| 109 | --- a/docs/observability.md |
| 110 | +++ b/docs/observability.md |
| 111 | @@ -44,6 +44,8 @@ func (*Telemetry) Queue(string, int) |
| 112 | |
| 113 | Operation, outcome, and queue labels are normalized to bounded vocabularies. Never add tenant IDs, pubkeys, event IDs, subscription IDs, raw filters, or arbitrary URLs to metric labels. Per-tenant inspection belongs in a separately authenticated, short-lived administrative view. |
| 114 | |
| 115 | +Tenant maintenance steps that touch storage report through `ObserveStorage` with their own operation name and an `ok` or `error` outcome. The `blob-expiry` operation is the sweep that removes agent uploads whose ttl has lapsed; its log line carries the tenant name and a count, never a hash or a key. |
| 116 | + |
| 117 | The registry exports relay metrics plus Go runtime collectors. The OTel tracer is always present and uses a ratio sampler; exporting is optional. When `OTLPEndpoint` is set, the package uses the pinned OTLP HTTP exporter and flushes spans during `Close`; no collector is required for the relay itself. |
| 118 | |
| 119 | Register durable work metrics explicitly after constructing the queue: |