Reviewed grant requests
| Field | Value |
|---|---|
| Identifier | agent-grant-requests |
| Status | Experimental |
| Contract version | 1; no negotiated wire version |
| Owner | internal/ request parsing, patching and replacement validation; internal/ admission/privacy; internal/ MCP preparation, approvals and history. |
| Wire | Agent-signed kind 1111 with request=grant; operator-signed kind 30392 replacement. |
The catalog's audit baseline and transport rules apply. NIP-22 supplies the kind 1111 comment/reference foundation; NIP-25 supplies kind 7 reactions; NIP-01 and NIP-40 supply signed event and expiration foundations. The grant request type, JSON patch, provenance and authority rules are project additions to those foundations. No upstream revision or full NIP-22 conformance audit is asserted. This is not the general-purpose approval convention or upstream delegation. See Agent identities for the workflow.
Request envelope
The request is signed by the agent named by the active current grant. Required tags are:
[
["E", "<current grant event ID>", "", "<operator pubkey>"],
["K", "30392"],
["P", "<operator pubkey>"],
["e", "<current grant event ID>", "", "<operator pubkey>"],
["k", "30392"],
["request", "grant"],
["grant", "{\"base\":\"<current grant event ID>\",\"changes\":{\"kinds\":[9]}}"],
["p", "<operator pubkey>"]
]
This is the builder's tag shape, with placeholders, not a signed event. The content is the reason: valid UTF-8, nonblank after trimming, at most 500 Unicode code points. Direct event validation counts the original content; the MCP builder trims it before constructing the event.
decodeGrantRequest and ReviewAgentGrantRequest enforce:
- Kind
1111and firstrequestvalue exactlygrant. - Only
request,grant,E,K,P,e,k,p,subjectandexpirationtag names. Any repeated tag name or unsupported/empty tag is rejected.subjectandexpirationare optional; no extension-specific subject length check is made. grantcontains one JSON value with fieldsbaseandchanges. Unknown struct fields or trailing JSON values are rejected. Duplicate JSON object keys are not separately rejected by the Go decoder; clients should emit unique keys.baseequals bothEande. The reference is 64 characters and must resolve to the current grant;Kandkare30392;Pand the onepname the current grant signer. Reference relay hints and additional positions are not fully NIP-22-validated here; clients should use the builder shape above.- The stored current grant belongs to the requesting agent and is active. A human role with a grant cannot use the narrow agent-request admission path. Requests without grants are rejected even when general comment publishing is allowed.
- At least one nonnull change field is present, and the resulting parsed scope differs from the original under ordered
reflect.DeepEqualcomparison. Empty arrays alone or adding only existing kinds/rooms cannot satisfy the no-op check. Repository/site replacements remove matching tags and append requested entries, so reordering alone can pass even when permissions are unchanged.
The optional positive expiration ends review eligibility. Malformed/nonpositive expiration values map to no expiry through event.Expiration; this parser does not add a stricter expiration syntax check. Ordinary event admission still rejects already expired requests.
Changes and replacement construction
| JSON field | Accepted value and operation |
|---|---|
kinds |
Integer array; add entries, retaining existing kinds. Resulting grant kinds must be from 0 through 65535. |
rooms |
String array; add entries. Each requested room must match ^[a-z0-9_-]{1,64}$, stricter than a direct grant's room parser. |
repos |
Objects with owner, identifier, level; replace entries matched by raw owner/identifier, retaining other repositories (see compatibility limits below). Repeated identities in a request are rejected. Resulting entries must pass the grant parser. |
sites |
Objects with label, optional integer ttl and boolean encrypted; replace entries matched by raw label, retaining others (see compatibility limits below). Duplicate labels and negative TTLs are rejected. Omitted/zero TTL means no TTL in the signed-event path; positive TTL is 1 through 365. The label must belong to the agent or be *. |
wiki |
String replacing the field: empty, propose or edit. |
jobs |
String replacing the field: empty, request, serve or both. |
rate |
Integer replacing the field, 1 through 600. |
All resulting lists and fields pass ParseAgentGrant, including its 256-entry caps. Null pointer fields mean no replacement. There is no request patch for the agent, operator, name, expiration or grant content. Kinds/rooms cannot remove entries; repository/site entries can change an existing permission, and empty wiki/jobs can clear those fields. “Additional access” is a use case, not a rule that every patch must increase permissions.
grantRequestTags copies retained tags in order, removes previous request provenance, adds requested kinds then rooms then repositories then sites, and appends supplied wiki/jobs/rate values. It then appends:
[
["grant-request", "<request event ID>"],
["grant-base", "<current grant event ID>"],
["e", "<request event ID>", "", "grant-request"]
]
The unsigned replacement retains the original operator, kind, name, expiration and content. Its ID/signature are cleared and its timestamp is advanced past the base grant if necessary. Clients should sign the reviewed template without reordering or normalizing tags: validation compares the complete ordered tag array as well as parsed scope, identity, name, expiration and original content.
Authority and lifecycle
request_grantoverPOST /withmcp reasonandchangesderives the current base/operator and returnsunsignedplusnext. This prepares but does not publish. Its schema is inmcp_grant_request.go.- The agent signs the event. Calling
request_grantwitheventrequires itspubkeyto match the authenticated actor, reviews it again and uses normal publication. WebSocketEVENTandPOST /enforce the same feature admission through the gate. This narrow path does not requireevents k=1111; the agent rate limit and host restrictions still apply. - The request can be read only by its author or the
poperator, subject to other read checks. Relay-owner/moderator standing alone does not bypass this participant check inGate.CanSee. Notifications direct the operator to review, not one-tap grant approval. - Approval is a separately signed replacement grant from that operator, who must still have grant authority. A
+reaction never changes access and does not settle the request as approved. Agents cannot approve themselves. - Any stored, unexpired kind 7 reaction from the operator with an
ereference to the request and trimmed content-blocks replacement. This is not a newest-reaction-wins rule. A later+does not undo a still-stored denial. - Replacement requires the request to be stored, not expired or denied; its base to remain current; the agent grant to remain unpaused, unrevoked and unexpired; and the exact reviewed replacement fields/tags. Validation runs at admission and again in
ApplyAgentEventTx, inside the event/grant transaction. A prepared template is not a reservation on current authority. - Approvals report
open,answeredwith decisionapproved/denied, orexpired. A superseded base produces a conflict and is rendered expired; inactive requests can carrygrant_errorwith no actionable review.grant_approvals.goconsults archived grant revisions so a later grant edit does not erase the historical approval receipt. A historical approved receipt does not prove the permission is still effective.
Private visibility is relay access control, not encryption. The operator and relay can read the reason and requested scope; signed copies may exist outside this relay. Do not put secrets in a rationale.
Errors and discovery
Error families include invalid: for malformed/no-op patches or replacement differences; restricted: for a missing/inactive grant, wrong requesting role or replacement operator; and conflict: for stale, expired or denied requests. Examples are invalid: grant request changes nothing, conflict: agent grant changed since the request and invalid: grant replacement tags differ from the reviewed request. MCP preparation/submission failures set isError and include an expected shape; an unsigned preview is not a successful write. Apply the transport error rules.
Discover request_grant with MCP tools/. browseapprovals and browseapproval are daemon read queries used by the approvals UI; the latter supplies the grant review (request_id, base, agent, operator, before, after, unsigned) or grant_error. There is no dedicated grant-request version capability; the relay's agents information entry describes grants generally, not caller eligibility.
Compatibility limits and evidence
- MCP's advertised plain-field schema requires nonempty wiki/jobs strings and a site TTL of at least 1, while signed-event review permits empty wiki/jobs and omitted/zero TTL. Do not assume schema acceptance equals event admission or that both paths expose every clearing operation.
- Direct grants trim repository strings and site labels, and repository identifiers may contain colons. The request patch remover matches raw tags without trimming and splits repository strings differently (
SplitN(..., 3)). A normalized repository/site identity may therefore fail to match the old tag; the retained entry takes precedence over the appended entry when parsed. This can make the request a rejected no-op or leave that replacement unapplied within an otherwise accepted patch. For example, an existing["sites", " * ", "ttl=7"]plus a request adding a new kind9and setting site*to TTL 3 is accepted but retains effective TTL 7. Inspect the reviewed effective scope, not just the requested changes. This audit documents the mismatches; it does not redefine identities or fix runtime behavior. - Request JSON and NIP-22 references have the decoder/positional validation limits noted above. No independently versioned JSON schema or exhaustive upstream conformance suite is claimed.
Evidence: community/ tests retained entries and patch ordering; grant_admission_test.go tests malformed requests, role/state restrictions, exact replacement and stale review; grant_approvals_test.go tests no-kind-1111 admission, participant reads, ineffective +, successful replacement/history, denial and supersession; mcp_grant_request_test.go tests operator-bound preparation, invalid changes and schema behavior.