clone

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

feat/tinyagent
tinyrelay / internal / daemon / mcp_grant_request.go
mcp_grant_request.go 4.8K
1package daemon23import (4 "context"5 "encoding/json"6 "errors"7 "strings"8 "time"9 "unicode/utf8"1011 "github.com/FelineStateMachine/tinyrelay/internal/community"12 "github.com/FelineStateMachine/tinyrelay/internal/event"13 "github.com/FelineStateMachine/tinyrelay/internal/mcp"14)1516const 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.`1718func mcpGrantRequestSchema() map[string]any {19 text := map[string]any{"type": "string", "minLength": 1}20 repo := mcp.Object(map[string]any{21 "owner": mcpPubKey, "identifier": text,22 "level": map[string]any{"type": "string", "enum": []string{community.AgentRepoPropose, community.AgentRepoRead, community.AgentRepoMaintain}},23 }, "owner", "identifier", "level")24 site := mcp.Object(map[string]any{"label": text, "ttl": map[string]any{"type": "integer", "minimum": 1, "maximum": community.AgentSiteTTLMax}, "encrypted": map[string]any{"type": "boolean"}}, "label")25 changes := mcp.Object(map[string]any{26 "kinds": map[string]any{"type": "array", "items": map[string]any{"type": "integer", "minimum": 0, "maximum": 65535}},27 "rooms": map[string]any{"type": "array", "items": text}, "repos": map[string]any{"type": "array", "items": repo},28 "sites": map[string]any{"type": "array", "items": site}, "wiki": text, "jobs": text,29 "rate": map[string]any{"type": "integer", "minimum": 1, "maximum": community.AgentRateMax},30 })31 return mcp.Object(map[string]any{"event": mcpEvent, "reason": map[string]any{"type": "string", "minLength": 1, "maxLength": 500}, "changes": changes})32}3334func (t *Tenant) mcpGrantRequest(ctx context.Context, call mcp.Call) (mcp.Result, error) {35 if raw, present := call.Arguments["event"]; present && raw != nil {36 encoded, err := json.Marshal(raw)37 if err != nil {38 return mcp.Result{}, err39 }40 e, err := event.Parse(encoded)41 if err == nil && e.PubKey != call.Actor {42 err = errors.New("the event pubkey must match the authenticated agent")43 }44 if err == nil {45 _, err = t.community.ReviewAgentGrantRequest(ctx, e, time.Now().Unix())46 }47 if err != nil {48 return mcp.Failure("The grant request is not acceptable: "+err.Error()+"\n"+mcpGrantRequestShape, map[string]any{"expected": mcpGrantRequestShape}), nil49 }50 return t.mcpPublish(ctx, call, e), nil51 }52 unsigned, err := t.mcpBuildGrantRequest(ctx, call)53 if err != nil {54 return mcp.Failure(err.Error()+"\n"+mcpGrantRequestShape, map[string]any{"expected": mcpGrantRequestShape}), nil55 }56 return mcp.Value(map[string]any{"unsigned": unsigned, "next": mcpSignNext}), nil57}5859func (t *Tenant) mcpBuildGrantRequest(ctx context.Context, call mcp.Call) (mcpUnsigned, error) {60 reason := strings.TrimSpace(call.String("reason"))61 if reason == "" || !utf8.ValidString(reason) || len([]rune(reason)) > 500 {62 return mcpUnsigned{}, errors.New("reason must be 1 to 500 valid UTF-8 characters")63 }64 grant, ok, err := t.community.AgentGrant(ctx, call.Actor)65 if err != nil {66 return mcpUnsigned{}, err67 }68 if !ok || !grant.Active(time.Now().Unix()) {69 return mcpUnsigned{}, errors.New("only an active granted agent can request more permissions")70 }71 changes, err := mcpGrantChanges(call.Arguments["changes"])72 if err != nil {73 return mcpUnsigned{}, err74 }75 grantJSON, err := json.Marshal(community.AgentGrantRequest{Base: grant.EventID, Changes: changes})76 if err != nil {77 return mcpUnsigned{}, err78 }79 tags := [][]string{{"E", grant.EventID, "", grant.Owner}, {"K", "30392"}, {"P", grant.Owner}, {"e", grant.EventID, "", grant.Owner}, {"k", "30392"}, {"request", "grant"}, {"grant", string(grantJSON)}, {"p", grant.Owner}}80 unsigned := mcpUnsigned{Kind: 1111, CreatedAt: time.Now().Unix(), Tags: tags, Content: reason}81 request := event.Event{PubKey: call.Actor, Kind: unsigned.Kind, CreatedAt: unsigned.CreatedAt, Tags: unsigned.Tags, Content: unsigned.Content}82 if _, err := t.community.ReviewAgentGrantRequest(ctx, request, time.Now().Unix()); err != nil {83 return mcpUnsigned{}, err84 }85 return unsigned, nil86}8788func mcpGrantChanges(raw any) (community.AgentGrantChanges, error) {89 encoded, err := json.Marshal(raw)90 if err != nil {91 return community.AgentGrantChanges{}, errors.New("changes must be an object")92 }93 var changes community.AgentGrantChanges94 decoder := json.NewDecoder(strings.NewReader(string(encoded)))95 decoder.DisallowUnknownFields()96 if err := decoder.Decode(&changes); err != nil {97 return community.AgentGrantChanges{}, errors.New("changes must contain only supported grant fields")98 }99 if changes.Kinds == nil && changes.Rooms == nil && changes.Repos == nil && changes.Sites == nil && changes.Wiki == nil && changes.Jobs == nil && changes.Rate == nil {100 return community.AgentGrantChanges{}, errors.New("changes must request at least one permission")101 }102 return changes, nil103}