tinyrelaycommit

Commit e83c920902fe

browse files at this commit | history

1diff --git a/internal/webui/webmcp.go b/internal/webui/webmcp.go
2index 85d12b1..84a48b5 100644
3--- a/internal/webui/webmcp.go
4+++ b/internal/webui/webmcp.go
5@@ -26,7 +26,7 @@ func (a *App) webMCPQuery(writer http.ResponseWriter, request *http.Request) {
6 return
7 }
8 }
9- allowed := map[string]bool{"browserepos": true, "browserepo": true, "browsefiles": true, "browsefile": true, "browsestatus": true, "browseissues": true, "browsepulls": true, "browseissue": true, "browsepull": true, "browsewiki": true, "browsewikipage": true, "browsewikimerge": true, "browserooms": true, "browseroom": true, "browsethread": true}
10+ allowed := map[string]bool{"browserepos": true, "browserepo": true, "browsefiles": true, "browsefile": true, "browsestatus": true, "browseissues": true, "browsepulls": true, "browseissue": true, "browsepull": true, "browsewiki": true, "browsewikipage": true, "browsewikimerge": true, "browserooms": true, "browseroom": true, "browsethread": true, "browseagent": true}
11 if !allowed[method] {
12 writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "unsupported WebMCP query"})
13 return
14diff --git a/internal/webui/webmcp.js b/internal/webui/webmcp.js
15index 1f3da64..3405416 100644
16--- a/internal/webui/webmcp.js
17+++ b/internal/webui/webmcp.js
18@@ -112,6 +112,11 @@
19 return manage(input.method, [], signal);
20 });
21
22+ register("tiny.list_agents", "List agent grants: name, public key, owner, scope, paused or revoked state, expiry and last event time. Uses your connected signer; owner or moderator.",
23+ object(), reads, (input, signal) => manage("listagents", [], signal));
24+ register("tiny.read_agent", "Read one agent's grant and its ten newest events. Requires a signed-in owner or moderator session.",
25+ object({agent: pubkey}, ["agent"]), reads, (input, signal) => query("browseagent", input, signal));
26+
27 function open(path) {
28 const url = new URL(local(path), location.href);
29 location.assign(url.href);
30@@ -156,6 +161,12 @@
31 every: {type: "integer", minimum: 0, description: "Interval in hours. Zero runs once."}, discoverPubKey: pubkey}, ["id", "kind"]),
32 "addjob", input => [input]);
33 control("tiny.remove_job", "Remove a job and cancel its pending runs.", object({id}, ["id"]), "removejob", input => [input.id]);
34+ control("tiny.pause_agent", "Pause an agent. Its grant stays and the relay rejects its events until it is resumed.",
35+ object({agent: pubkey}, ["agent"]), "pauseagent", input => [input.agent]);
36+ control("tiny.resume_agent", "Resume a paused agent so it may publish again.",
37+ object({agent: pubkey}, ["agent"]), "resumeagent", input => [input.agent]);
38+ control("tiny.revoke_agent", "Revoke an agent's grant. The agent loses its role at once; only a new grant restores it.",
39+ object({agent: pubkey}, ["agent"]), "revokeagent", input => [input.agent]);
40 control("tiny.backup_now", "Queue a backup of relay data. Inspect jobs and backups to check completion.", object(), "backupnow", () => []);
41 control("tiny.dump_now", "Queue an event export. Inspect jobs and dumps to check completion.", object(), "dumpnow", () => []);
42 control("tiny.set_connections", "Replace the relay's connection list. Read the current list before editing it.",
43diff --git a/scripts/webmcp.test.mjs b/scripts/webmcp.test.mjs
44index 06e4c9b..f18c63c 100644
45--- a/scripts/webmcp.test.mjs
46+++ b/scripts/webmcp.test.mjs
47@@ -152,3 +152,33 @@ test("link, files and notification tools reach the new surfaces", async () => {
48 assert.equal(calls[0].url, "/r/work/manage/rpc");
49 assert.equal(calls[0].body.method, "notifytest");
50 });
51+
52+test("agent tools list through the signer, read through the session and control through management", async () => {
53+ const calls = [], queries = [];
54+ const {tools} = await browser({path: "/r/work/manage/agents", fetch: async url => {
55+ queries.push(new URL(url, "https://tiny.example"));
56+ return new Response(JSON.stringify({agent: {name: "hermes"}, events: []}));
57+ }, signedFetch: async (url, method, body) => {
58+ calls.push({url, method, body: JSON.parse(body)});
59+ return new Response(JSON.stringify({result: [{name: "hermes", paused: false}]}));
60+ }});
61+ const agent = "a".repeat(64);
62+ const list = tools.get("tiny.list_agents");
63+ assert.equal(list.annotations.readOnlyHint, true);
64+ const listed = await list.execute({});
65+ assert.equal(listed.structuredContent.result[0].name, "hermes");
66+ assert.deepEqual(calls[0].body, {method: "listagents", params: []});
67+ assert.equal(calls[0].url, "/r/work/manage/rpc");
68+ const read = await tools.get("tiny.read_agent").execute({agent});
69+ assert.equal(queries[0].pathname, "/r/work/webmcp/query");
70+ assert.equal(queries[0].searchParams.get("method"), "browseagent");
71+ assert.deepEqual(JSON.parse(queries[0].searchParams.get("params")), [{agent}]);
72+ assert.equal(read.structuredContent.result.agent.name, "hermes");
73+ for (const [name, method] of [["tiny.pause_agent", "pauseagent"], ["tiny.resume_agent", "resumeagent"], ["tiny.revoke_agent", "revokeagent"]]) {
74+ const tool = tools.get(name);
75+ assert.equal(tool.annotations.consequentialHint, true);
76+ assert.deepEqual([...tool.inputSchema.required], ["agent"]);
77+ await tool.execute({agent});
78+ assert.deepEqual(calls.at(-1).body, {method, params: [agent]});
79+ }
80+});

About

ownerc01503e8035f
visibilitypublic
heade83c920902fe

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs