clone

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

add access request tools to the browser agent

Dami committed 1b87a28
commit
1b87a28f6ec644b3ace669ca4264061623b9506e
parent
6d1dafb4feb3215a1661dee89c8ac814e703fb9e
author
Dami <dami.etoile@gmail.com>
2 files changed, +36 -0
Minternal/webui/webmcp.js6++++++
Mscripts/webmcp.test.mjs30++++++++++++++++++++++++++++++
M internal/webui/webmcp.js +6 -0
@@ -145,6 +145,8 @@
145145 object({agent: pubkey}, ["agent"]), reads, (input, signal) => query("browseagent", input, signal));146146 register("tiny.list_callbacks", "List event callbacks: id, owner, host, filter, paused state, failures and last delivery. Members and agents see their own; the owner and moderators see every callback. Uses your connected signer.",147147 object(), reads, (input, signal) => manage("listcallbacks", [], signal));148+ register("tiny.list_join_requests", "List access requests from people who asked to join without an invite: pubkey, reason, time asked, status (pending, approved or denied), who decided and when. Pending requests come first. Uses your connected signer; owner or moderator.",149+ object(), reads, (input, signal) => manage("listjoinrequests", [], signal));148150 149151 function open(path) {150152 const url = new URL(local(path), location.href);
@@ -232,6 +234,10 @@
232234 object({agent: pubkey}, ["agent"]), "resumeagent", input => [input.agent]);233235 control("tiny.revoke_agent", "Revoke an agent's grant. The agent loses its role at once; only a new grant restores it.",234236 object({agent: pubkey}, ["agent"]), "revokeagent", input => [input.agent]);237+ control("tiny.approve_join", "Approve an access request: the key becomes a member, the relay's member list and add-user record follow, and the request is marked approved. Owner or moderator.",238+ object({pubkey}, ["pubkey"]), "approvejoin", input => [input.pubkey]);239+ control("tiny.deny_join", "Deny an access request. The key stays outside the relay and may ask again. Owner or moderator.",240+ object({pubkey}, ["pubkey"]), "denyjoin", input => [input.pubkey]);235241 control("tiny.pause_callback", "Pause an event callback by id. Deliveries stop until it is resumed. The callback's owner, the relay owner and moderators may do this.",236242 object({id}, ["id"]), "pausecallback", input => [input.id]);237243 control("tiny.resume_callback", "Resume a paused event callback by id and clear its failure count.",
M scripts/webmcp.test.mjs +30 -0
@@ -238,6 +238,36 @@ test("agent tools list through the signer, read through the session and control
238238 }239239 });240240 241+test("access request tools list through the signer and decide by pubkey", async () => {242+ const calls = [];243+ const asker = "b".repeat(64);244+ const {tools} = await browser({path: "/r/work/manage/people", signedFetch: async (url, method, body) => {245+ calls.push({url, method, body: JSON.parse(body)});246+ const request = JSON.parse(body);247+ if (request.method === "listjoinrequests") {248+ return new Response(JSON.stringify({result: [{pubkey: asker, reason: "Met you at the meetup", requested_at: 1700000000, status: "pending", decided_by: "", decided_at: 0}]}));249+ }250+ return new Response(JSON.stringify({result: {pubkey: asker, status: request.method === "approvejoin" ? "approved" : "denied"}}));251+ }});252+ const list = tools.get("tiny.list_join_requests");253+ assert.equal(list.annotations.readOnlyHint, true);254+ const listed = await list.execute({});255+ assert.equal(listed.structuredContent.result[0].pubkey, asker);256+ assert.equal(listed.structuredContent.result[0].status, "pending");257+ assert.equal(calls[0].url, "/r/work/manage/rpc");258+ assert.equal(calls[0].method, "POST");259+ assert.deepEqual(calls[0].body, {method: "listjoinrequests", params: []});260+ for (const [name, method, status] of [["tiny.approve_join", "approvejoin", "approved"], ["tiny.deny_join", "denyjoin", "denied"]]) {261+ const tool = tools.get(name);262+ assert.equal(tool.annotations.consequentialHint, true);263+ assert.deepEqual([...tool.inputSchema.required], ["pubkey"]);264+ assert.equal(tool.inputSchema.properties.pubkey.pattern, "^[0-9a-f]{64}$");265+ const result = await tool.execute({pubkey: asker});266+ assert.deepEqual(calls.at(-1).body, {method, params: [asker]});267+ assert.equal(result.structuredContent.result.status, status);268+ }269+});270+241271 test("callback tools list through the signer and control by id", async () => {242272 const calls = [];243273 const {tools} = await browser({path: "/r/work/manage/agents", signedFetch: async (url, method, body) => {