tinyrelayfile
tinyrelay/internal/daemon/mcp_grant_request.go/

internal/daemon/mcp_grant_request.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "time"
9 "unicode/utf8"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/community"
12 "github.com/FelineStateMachine/tinyrelay/internal/event"
13 "github.com/FelineStateMachine/tinyrelay/internal/mcp"
14)
15
16const 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.`
17
18func 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}
33
34func (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{}, err
39 }
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}), nil
49 }
50 return t.mcpPublish(ctx, call, e), nil
51 }
52 unsigned, err := t.mcpBuildGrantRequest(ctx, call)
53 if err != nil {
54 return mcp.Failure(err.Error()+"\n"+mcpGrantRequestShape, map[string]any{"expected": mcpGrantRequestShape}), nil
55 }
56 return mcp.Value(map[string]any{"unsigned": unsigned, "next": mcpSignNext}), nil
57}
58
59func (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{}, err
67 }
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{}, err
74 }
75 grantJSON, err := json.Marshal(community.AgentGrantRequest{Base: grant.EventID, Changes: changes})
76 if err != nil {
77 return mcpUnsigned{}, err
78 }
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{}, err
84 }
85 return unsigned, nil
86}
87
88func 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.AgentGrantChanges
94 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, nil
103}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs