- commit
- e83c920902fe954e262fa70a07e2ff880b8580f5
- parent
- 1351f00ce7194a1e524ec9f57707aaafb6e99ccb
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 2 | +- |
| M | internal/ | 11 | +++++++++++ |
| M | scripts/ | 30 | ++++++++++++++++++++++++++++++ |
@@ -26,7 +26,7 @@ func (a *App) webMCPQuery(writer http.ResponseWriter, request *http.Request) {
2626 return2727 }2828 }29- 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}29+ 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}3030 if !allowed[method] {3131 writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "unsupported WebMCP query"})3232 return@@ -112,6 +112,11 @@
112112 return manage(input.method, [], signal);113113 });114114 115+ 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.",116+ object(), reads, (input, signal) => manage("listagents", [], signal));117+ register("tiny.read_agent", "Read one agent's grant and its ten newest events. Requires a signed-in owner or moderator session.",118+ object({agent: pubkey}, ["agent"]), reads, (input, signal) => query("browseagent", input, signal));119+115120 function open(path) {116121 const url = new URL(local(path), location.href);117122 location.assign(url.href);@@ -156,6 +161,12 @@
156161 every: {type: "integer", minimum: 0, description: "Interval in hours. Zero runs once."}, discoverPubKey: pubkey}, ["id", "kind"]),157162 "addjob", input => [input]);158163 control("tiny.remove_job", "Remove a job and cancel its pending runs.", object({id}, ["id"]), "removejob", input => [input.id]);164+ control("tiny.pause_agent", "Pause an agent. Its grant stays and the relay rejects its events until it is resumed.",165+ object({agent: pubkey}, ["agent"]), "pauseagent", input => [input.agent]);166+ control("tiny.resume_agent", "Resume a paused agent so it may publish again.",167+ object({agent: pubkey}, ["agent"]), "resumeagent", input => [input.agent]);168+ control("tiny.revoke_agent", "Revoke an agent's grant. The agent loses its role at once; only a new grant restores it.",169+ object({agent: pubkey}, ["agent"]), "revokeagent", input => [input.agent]);159170 control("tiny.backup_now", "Queue a backup of relay data. Inspect jobs and backups to check completion.", object(), "backupnow", () => []);160171 control("tiny.dump_now", "Queue an event export. Inspect jobs and dumps to check completion.", object(), "dumpnow", () => []);161172 control("tiny.set_connections", "Replace the relay's connection list. Read the current list before editing it.",@@ -152,3 +152,33 @@ test("link, files and notification tools reach the new surfaces", async () => {
152152 assert.equal(calls[0].url, "/r/work/manage/rpc");153153 assert.equal(calls[0].body.method, "notifytest");154154 });155+156+test("agent tools list through the signer, read through the session and control through management", async () => {157+ const calls = [], queries = [];158+ const {tools} = await browser({path: "/r/work/manage/agents", fetch: async url => {159+ queries.push(new URL(url, "https://tiny.example"));160+ return new Response(JSON.stringify({agent: {name: "hermes"}, events: []}));161+ }, signedFetch: async (url, method, body) => {162+ calls.push({url, method, body: JSON.parse(body)});163+ return new Response(JSON.stringify({result: [{name: "hermes", paused: false}]}));164+ }});165+ const agent = "a".repeat(64);166+ const list = tools.get("tiny.list_agents");167+ assert.equal(list.annotations.readOnlyHint, true);168+ const listed = await list.execute({});169+ assert.equal(listed.structuredContent.result[0].name, "hermes");170+ assert.deepEqual(calls[0].body, {method: "listagents", params: []});171+ assert.equal(calls[0].url, "/r/work/manage/rpc");172+ const read = await tools.get("tiny.read_agent").execute({agent});173+ assert.equal(queries[0].pathname, "/r/work/webmcp/query");174+ assert.equal(queries[0].searchParams.get("method"), "browseagent");175+ assert.deepEqual(JSON.parse(queries[0].searchParams.get("params")), [{agent}]);176+ assert.equal(read.structuredContent.result.agent.name, "hermes");177+ for (const [name, method] of [["tiny.pause_agent", "pauseagent"], ["tiny.resume_agent", "resumeagent"], ["tiny.revoke_agent", "revokeagent"]]) {178+ const tool = tools.get(name);179+ assert.equal(tool.annotations.consequentialHint, true);180+ assert.deepEqual([...tool.inputSchema.required], ["agent"]);181+ await tool.execute({agent});182+ assert.deepEqual(calls.at(-1).body, {method, params: [agent]});183+ }184+});