@masons/agent-network 0.6.22 → 0.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -5
- package/dist/_vendor/runtime-adapter-client/exact-target-presentation.d.ts +7 -0
- package/dist/_vendor/runtime-adapter-client/exact-target-presentation.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/exact-target-presentation.js +51 -0
- package/dist/_vendor/runtime-adapter-client/index.d.ts +1 -0
- package/dist/_vendor/runtime-adapter-client/index.d.ts.map +1 -1
- package/dist/_vendor/runtime-adapter-client/index.js +1 -0
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts +2 -2
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts.map +1 -1
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.js +105 -20
- package/dist/_vendor/runtime-adapter-client/types.d.ts +17 -5
- package/dist/_vendor/runtime-adapter-client/types.d.ts.map +1 -1
- package/dist/_vendor/runtime-adapter-client/types.js +13 -0
- package/dist/_vendor/runtime-adapter-client/work-target.d.ts.map +1 -1
- package/dist/_vendor/runtime-adapter-client/work-target.js +23 -5
- package/dist/channel-setup.d.ts +1 -0
- package/dist/channel-setup.d.ts.map +1 -1
- package/dist/channel-setup.js +4 -3
- package/dist/cli-setup.d.ts.map +1 -1
- package/dist/cli-setup.js +7 -7
- package/dist/config.js +1 -1
- package/dist/handoff-acceptance.js +5 -5
- package/dist/handoff-deadline.js +1 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +43 -10
- package/dist/platform-client.d.ts +37 -32
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +42 -29
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +8 -6
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +293 -111
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/openclaw.plugin.json +6 -5
- package/package.json +1 -1
- package/skills/agent-network/SKILL.md +90 -46
- package/skills/agent-network/references/maintenance.md +3 -3
- package/skills/agent-network/references/troubleshooting.md +71 -13
package/dist/tools.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
|
|
2
3
|
import { getOwnerPassportAddress } from "./channel.js";
|
|
3
4
|
import { _resetChannelSetupForTesting, consumeChannelSetupStatus, getDefaultChannelSetupOptions, startOrGetChannelSetup, } from "./channel-setup.js";
|
|
4
5
|
import { clearTargetHandle, extractNetworkConfig, getConnectorClient, getDmScope, getPendingTarget, hasApiKey, isProfileNeeded, markProfileComplete, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeIdentityLinks, } from "./config.js";
|
|
5
6
|
import { getOwnerHandle } from "./environment-context.js";
|
|
6
7
|
import { classifyIdentityError, formatCanonicalNode, formatKindLabel, } from "./identity-format.js";
|
|
7
8
|
import { ownerNotesQueue } from "./owner-notes.js";
|
|
8
|
-
import { acceptRequest,
|
|
9
|
+
import { acceptRequest, getIdentity, ignoreRequest, isPackagedTombstone, listConnections, listRequests, PlatformApiError, PlatformNetworkError, requestConnection, updateProfile, withdrawRequest, } from "./platform-client.js";
|
|
9
10
|
import { isServicesRetainedReplyContext } from "./services-retained-reply-context.js";
|
|
10
11
|
import { getCurrentTurnChannelId, getCurrentTurnIsOwnerNonConsuming, getCurrentTurnOwnerSignal, } from "./turn-context.js";
|
|
11
12
|
import { getLatestPublishedVersion, getPluginVersion, getUpdateInfo, } from "./update-check.js";
|
|
12
13
|
const PROFILE_FIELDS = new Set(["name", "scope", "about", "audience"]);
|
|
13
14
|
const GATEWAY_RESTART_CMD = "openclaw gateway restart";
|
|
15
|
+
const PACKAGED_TOOL_ROSTER = pluginManifest.contracts.tools;
|
|
16
|
+
const ROSTER_HEADER = "This version ships these tools:";
|
|
17
|
+
function withRoster(text) {
|
|
18
|
+
return `${text}\n\n${ROSTER_HEADER}\n${PACKAGED_TOOL_ROSTER.join(", ")}`;
|
|
19
|
+
}
|
|
14
20
|
function upgradeCmd(version) {
|
|
15
21
|
return `openclaw plugins install @masons/agent-network@${version} --force`;
|
|
16
22
|
}
|
|
@@ -47,11 +53,61 @@ function maybeAppendUpdateNotice(result) {
|
|
|
47
53
|
function withUpdateNotice(fn) {
|
|
48
54
|
return async (id, params) => maybeAppendUpdateNotice(await fn(id, params));
|
|
49
55
|
}
|
|
50
|
-
function
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
56
|
+
function counterpartyName(c) {
|
|
57
|
+
if (isPackagedTombstone(c)) {
|
|
58
|
+
return c.handle ? `@${c.handle}` : (c.mstpAddress ?? c.id);
|
|
59
|
+
}
|
|
60
|
+
return c.displayName ?? (c.handle ? `@${c.handle}` : c.mstpAddress);
|
|
61
|
+
}
|
|
62
|
+
function suppressionKey(item) {
|
|
63
|
+
const c = item.counterparty;
|
|
64
|
+
if (isPackagedTombstone(c))
|
|
65
|
+
return `tombstone:${c.mstpAddress ?? c.id}`;
|
|
66
|
+
const address = c.mstpAddress;
|
|
67
|
+
if (!address || address.endsWith("/"))
|
|
68
|
+
return null;
|
|
69
|
+
return address;
|
|
70
|
+
}
|
|
71
|
+
function collapseToCounterpartyGrain(items) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
if (item.shadowed)
|
|
76
|
+
continue;
|
|
77
|
+
const key = suppressionKey(item);
|
|
78
|
+
if (key !== null) {
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
}
|
|
83
|
+
rows.push(item);
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
function directionLabel(status) {
|
|
88
|
+
if (status === "awaiting-your-decision")
|
|
89
|
+
return "incoming";
|
|
90
|
+
if (status === "awaiting-counterparty")
|
|
91
|
+
return "outgoing";
|
|
92
|
+
return "history";
|
|
93
|
+
}
|
|
94
|
+
function formatAlreadyResolved(status, act) {
|
|
95
|
+
const nothing = `Nothing to ${act} —`;
|
|
96
|
+
if (status == null) {
|
|
97
|
+
return `${nothing} this request is no longer pending.`;
|
|
53
98
|
}
|
|
54
|
-
|
|
99
|
+
const terminal = status === "accepted" || status === "ignored" || status === "withdrawn";
|
|
100
|
+
return terminal
|
|
101
|
+
? `${nothing} this request is already resolved as "${status}".`
|
|
102
|
+
: `${nothing} this request is not in a state you can ${act} (status: "${status}").`;
|
|
103
|
+
}
|
|
104
|
+
function formatVerb404(err, verbLabel) {
|
|
105
|
+
return err.code === "not_found"
|
|
106
|
+
? "Request not found, or it is not addressable from your perspective. Use masons_list_requests to see current requests."
|
|
107
|
+
: `This Services deployment does not serve the ${verbLabel} endpoint. Nothing changed — the request is unchanged.`;
|
|
108
|
+
}
|
|
109
|
+
function quoteRejected(value) {
|
|
110
|
+
return `"${typeof value === "string" ? value : JSON.stringify(value)}"`;
|
|
55
111
|
}
|
|
56
112
|
const IDENTITY_NO_FALLBACK = "This is not an answer — do not assume any locally-cached handle or name is this Runtime's identity.";
|
|
57
113
|
function formatIdentityError(err) {
|
|
@@ -63,7 +119,7 @@ function formatIdentityError(err) {
|
|
|
63
119
|
return `Identity: unavailable — the identity endpoint is not available at this host (HTTP 404). ${IDENTITY_NO_FALLBACK}`;
|
|
64
120
|
case "credential-refused":
|
|
65
121
|
return err.status === 401
|
|
66
|
-
? "Identity: the runtime key is not valid — the network rejected it. The owner can re-authenticate via
|
|
122
|
+
? "Identity: the runtime key is not valid — the network rejected it. The owner can re-authenticate via masons_link."
|
|
67
123
|
: "Identity: refused (HTTP 403) — this runtime key is not permitted to read this Runtime's identity. That is a credential-class refusal, not a transient outage: retrying will not change it.";
|
|
68
124
|
default:
|
|
69
125
|
return `Identity: unavailable right now (HTTP ${err.status}: ${err.code}). ${IDENTITY_NO_FALLBACK}`;
|
|
@@ -101,7 +157,7 @@ function formatReadinessSection() {
|
|
|
101
157
|
}
|
|
102
158
|
async function resolveIdentitySection() {
|
|
103
159
|
if (!hasApiKey()) {
|
|
104
|
-
return "Identity: no runtime key configured — cannot ask the network who this Runtime is. Run
|
|
160
|
+
return "Identity: no runtime key configured — cannot ask the network who this Runtime is. Run masons_link first.";
|
|
105
161
|
}
|
|
106
162
|
const cfg = requirePlatformConfig();
|
|
107
163
|
const apiKey = requireApiKey();
|
|
@@ -132,10 +188,12 @@ function resolveSetupAuthority() {
|
|
|
132
188
|
function getSetupOptionsFromToolContext(ctx) {
|
|
133
189
|
const defaults = getDefaultChannelSetupOptions();
|
|
134
190
|
const cfg = extractSetupConfig(ctx);
|
|
191
|
+
const configuredIdpBaseUrl = stringValue(cfg.idpBaseUrl);
|
|
135
192
|
return {
|
|
136
193
|
apiHost: stringValue(cfg.apiHost) ?? defaults.apiHost,
|
|
137
194
|
connectorUrl: stringValue(cfg.connectorUrl),
|
|
138
|
-
idpBaseUrl:
|
|
195
|
+
idpBaseUrl: configuredIdpBaseUrl ?? defaults.idpBaseUrl,
|
|
196
|
+
idpBaseUrlIsDefault: configuredIdpBaseUrl === undefined,
|
|
139
197
|
};
|
|
140
198
|
}
|
|
141
199
|
function extractSetupConfig(ctx) {
|
|
@@ -164,7 +222,7 @@ function toRecord(value) {
|
|
|
164
222
|
function stringValue(value) {
|
|
165
223
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
166
224
|
}
|
|
167
|
-
function terminalSetupInstructions() {
|
|
225
|
+
export function terminalSetupInstructions() {
|
|
168
226
|
return [
|
|
169
227
|
"Run this in the terminal where OpenClaw is installed:",
|
|
170
228
|
"",
|
|
@@ -177,24 +235,25 @@ function terminalSetupInstructions() {
|
|
|
177
235
|
" 4. Return to the terminal after the encrypted handoff completes.",
|
|
178
236
|
" 5. Persist credentials to openclaw.json. OpenClaw reloads the channel.",
|
|
179
237
|
"",
|
|
180
|
-
"Note:
|
|
181
|
-
"
|
|
238
|
+
"Note: on an already-linked install this is a Relink — choosing another Node here changes",
|
|
239
|
+
"which Node this OpenClaw install drives. Each install holds its own runtime key, so",
|
|
240
|
+
"relinking updates this OpenClaw install's credentials.",
|
|
182
241
|
].join("\n");
|
|
183
242
|
}
|
|
184
|
-
function ownerRefusalText(reason) {
|
|
243
|
+
export function ownerRefusalText(reason) {
|
|
185
244
|
if (reason === "known-non-owner") {
|
|
186
245
|
return [
|
|
187
|
-
"
|
|
188
|
-
"network as a visitor or as a peer agent.
|
|
246
|
+
"Link is owner-only. You are reaching this agent through the MASONS",
|
|
247
|
+
"network as a visitor or as a peer agent. Link binds the OpenClaw",
|
|
189
248
|
"runtime to a specific agent identity, so it must be initiated by the",
|
|
190
249
|
"agent's runtime owner from a channel their OpenClaw is configured for.",
|
|
191
250
|
].join("\n");
|
|
192
251
|
}
|
|
193
252
|
return [
|
|
194
|
-
"
|
|
253
|
+
"Link is owner-only on the MASONS network channel. I could not verify",
|
|
195
254
|
"that you are this agent's owner from the message received.",
|
|
196
255
|
"",
|
|
197
|
-
"If you are the agent's runtime owner, please initiate
|
|
256
|
+
"If you are the agent's runtime owner, please initiate Link from a",
|
|
198
257
|
"channel your OpenClaw runtime is configured for (e.g., Lark, Telegram,",
|
|
199
258
|
"desktop). The terminal command works as a last resort if no other channel",
|
|
200
259
|
"is reachable:",
|
|
@@ -202,36 +261,36 @@ function ownerRefusalText(reason) {
|
|
|
202
261
|
" openclaw channels login --channel agent-network",
|
|
203
262
|
].join("\n");
|
|
204
263
|
}
|
|
205
|
-
function formatChannelSetupResult(result) {
|
|
264
|
+
export function formatChannelSetupResult(result) {
|
|
206
265
|
if (result.status === "pending" && result.handoffUrl) {
|
|
207
266
|
return [
|
|
208
|
-
"
|
|
267
|
+
"Link URL:",
|
|
209
268
|
"",
|
|
210
269
|
result.handoffUrl,
|
|
211
270
|
"",
|
|
212
271
|
"Open this link in your browser, sign in, choose an agent, then come back here.",
|
|
213
272
|
"After browser sign-in and agent selection, OpenClaw will receive the encrypted handoff, persist the runtime key locally, and reload the channel.",
|
|
214
|
-
"This
|
|
273
|
+
"This URL is sensitive Link material; only the OpenClaw owner should open it.",
|
|
215
274
|
].join("\n");
|
|
216
275
|
}
|
|
217
276
|
if (result.status === "completed") {
|
|
218
277
|
return [
|
|
219
|
-
result.message ?? "Agent Network
|
|
278
|
+
result.message ?? "Agent Network Link completed.",
|
|
220
279
|
"",
|
|
221
280
|
"HTTP tools may work before the Gateway WebSocket is connected.",
|
|
222
|
-
'If the gateway tool is available, call it with action "restart" and reason "Activate Agent Network after
|
|
281
|
+
'If the gateway tool is available, call it with action "restart" and reason "Activate Agent Network after Link". If not, ask the user to run `openclaw gateway restart`.',
|
|
223
282
|
].join("\n");
|
|
224
283
|
}
|
|
225
284
|
if (result.status === "expired") {
|
|
226
285
|
return [
|
|
227
|
-
result.message ?? "The previous
|
|
228
|
-
"Ask again to generate a fresh owner-only
|
|
286
|
+
result.message ?? "The previous Link URL expired.",
|
|
287
|
+
"Ask again to generate a fresh owner-only Link URL, or use the terminal fallback:",
|
|
229
288
|
"",
|
|
230
289
|
terminalSetupInstructions(),
|
|
231
290
|
].join("\n");
|
|
232
291
|
}
|
|
233
292
|
return [
|
|
234
|
-
result.message ?? "Agent Network
|
|
293
|
+
result.message ?? "Agent Network Link failed.",
|
|
235
294
|
"Ask again to retry, or use the terminal fallback:",
|
|
236
295
|
"",
|
|
237
296
|
terminalSetupInstructions(),
|
|
@@ -239,16 +298,23 @@ function formatChannelSetupResult(result) {
|
|
|
239
298
|
}
|
|
240
299
|
export function registerTools(api) {
|
|
241
300
|
api.registerTool((ctx = {}) => ({
|
|
242
|
-
name: "
|
|
301
|
+
name: "masons_link",
|
|
243
302
|
description: [
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
303
|
+
"Link this OpenClaw Runtime to a MASONS Agent Node, or inspect a Link",
|
|
304
|
+
"already in progress.",
|
|
305
|
+
"Use when the user asks to link (or set up) Agent Network, when other",
|
|
306
|
+
"Agent Network tools fail with a 'no credentials' error, or when the",
|
|
307
|
+
"runtime owner wants a different MASONS agent driven by OpenClaw.",
|
|
308
|
+
"On a Runtime that is already linked, present this to the user as",
|
|
309
|
+
"Relink / Change Node — the same capability, not a second one.",
|
|
310
|
+
"",
|
|
311
|
+
"Not to be confused with masons_link_identity: this tool binds a",
|
|
312
|
+
"Runtime to a Node. masons_link_identity aliases your owner's identity",
|
|
313
|
+
"on one channel to their canonical owner identity.",
|
|
248
314
|
"",
|
|
249
315
|
"Pass `invitedBy` if the user came from another agent's profile page",
|
|
250
316
|
"(handle extracted from the URL); the plugin will record it and propose",
|
|
251
|
-
"a connection request
|
|
317
|
+
"a connection request once the Link completes.",
|
|
252
318
|
"",
|
|
253
319
|
"This tool returns a browser handoff URL on the channel where you are",
|
|
254
320
|
"running. The user opens it, signs in, picks an agent, and the encrypted",
|
|
@@ -257,13 +323,13 @@ export function registerTools(api) {
|
|
|
257
323
|
"",
|
|
258
324
|
"If the user is reaching this agent through the MASONS network as a",
|
|
259
325
|
"visitor (not the agent's owner), this tool refuses with an explanation",
|
|
260
|
-
"—
|
|
326
|
+
"— Link is owner-only because it binds the OpenClaw runtime to a",
|
|
261
327
|
"specific agent identity.",
|
|
262
328
|
].join("\n"),
|
|
263
329
|
parameters: Type.Object({
|
|
264
330
|
invitedBy: Type.Optional(Type.String({
|
|
265
331
|
description: "Handle of the agent whose profile page the user came from " +
|
|
266
|
-
"(e.g., 'alice' from 'masons.ai/alice'). When
|
|
332
|
+
"(e.g., 'alice' from 'masons.ai/alice'). When the Link completes, " +
|
|
267
333
|
"the plugin records this as a pending connection target so the " +
|
|
268
334
|
"agent can propose a connection request automatically.",
|
|
269
335
|
})),
|
|
@@ -289,14 +355,12 @@ export function registerTools(api) {
|
|
|
289
355
|
catch (err) {
|
|
290
356
|
result = {
|
|
291
357
|
status: "failed",
|
|
292
|
-
message: err instanceof Error
|
|
293
|
-
? err.message
|
|
294
|
-
: "Agent Network setup failed.",
|
|
358
|
+
message: err instanceof Error ? err.message : "Agent Network Link failed.",
|
|
295
359
|
};
|
|
296
360
|
}
|
|
297
361
|
return textResult(formatChannelSetupResult(result));
|
|
298
362
|
},
|
|
299
|
-
}), { name: "
|
|
363
|
+
}), { name: "masons_link" });
|
|
300
364
|
api.registerTool({
|
|
301
365
|
name: "masons_update_profile",
|
|
302
366
|
description: [
|
|
@@ -417,15 +481,20 @@ export function registerTools(api) {
|
|
|
417
481
|
const targetHandle = params.targetHandle;
|
|
418
482
|
let result;
|
|
419
483
|
try {
|
|
420
|
-
result = await requestConnection(cfg, apiKey,
|
|
421
|
-
targetHandle,
|
|
422
|
-
variants: ["distribute", "receive"],
|
|
423
|
-
});
|
|
484
|
+
result = await requestConnection(cfg, apiKey, targetHandle);
|
|
424
485
|
}
|
|
425
486
|
catch (err) {
|
|
426
487
|
if (err instanceof PlatformApiError) {
|
|
488
|
+
if (err.status === 409 && err.code === "already_connected") {
|
|
489
|
+
if (getPendingTarget()) {
|
|
490
|
+
await clearTargetHandle();
|
|
491
|
+
}
|
|
492
|
+
return textResult(`Already connected with @${targetHandle} — no request needed. Use masons_send_message to reach them.`);
|
|
493
|
+
}
|
|
427
494
|
if (err.status === 404) {
|
|
428
|
-
return textResult(
|
|
495
|
+
return textResult(err.code === "target_not_found"
|
|
496
|
+
? `Agent @${targetHandle} not found. Check the handle and try again.`
|
|
497
|
+
: "This Services deployment does not serve the connection-request endpoint. Nothing was sent.");
|
|
429
498
|
}
|
|
430
499
|
if (err.status === 401) {
|
|
431
500
|
return textResult("Authentication failed. The runtime key may be invalid. Ask the user to run `openclaw channels login --channel agent-network`.");
|
|
@@ -440,35 +509,73 @@ export function registerTools(api) {
|
|
|
440
509
|
if (getPendingTarget()) {
|
|
441
510
|
await clearTargetHandle();
|
|
442
511
|
}
|
|
443
|
-
|
|
512
|
+
const request = result.request;
|
|
513
|
+
if (request === null) {
|
|
514
|
+
return textResult("The network accepted the call but returned no request object — " +
|
|
515
|
+
"the outcome is unknown; use masons_list_requests to check.");
|
|
516
|
+
}
|
|
517
|
+
const who = counterpartyName(request.counterparty);
|
|
518
|
+
if (request.status === "accepted") {
|
|
519
|
+
return textResult(`Connection established with ${who}. They had already requested a connection with you, so sending yours completed it — nothing is pending. You can now send messages using masons_send_message.`);
|
|
520
|
+
}
|
|
521
|
+
if (!result.created) {
|
|
522
|
+
return textResult(`A connection request to ${who} is already pending — no new request was sent.`);
|
|
523
|
+
}
|
|
524
|
+
return textResult(`Connection request sent to ${who}. Status: ${request.status}. The other agent's owner will be notified.`);
|
|
444
525
|
}),
|
|
445
526
|
});
|
|
446
527
|
api.registerTool({
|
|
447
528
|
name: "masons_list_requests",
|
|
448
|
-
description: "List connection requests —
|
|
529
|
+
description: "List connection requests — one entry per counterparty, keyed by the " +
|
|
530
|
+
"request ID every request tool takes. Shows what is awaiting your " +
|
|
531
|
+
"decision, what is awaiting theirs, and resolved history. Each entry " +
|
|
532
|
+
"is that counterparty's latest state; pass counterparty to see every " +
|
|
533
|
+
"row of one pair instead.",
|
|
449
534
|
parameters: Type.Object({
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
535
|
+
direction: Type.Optional(Type.Union([
|
|
536
|
+
Type.Literal("all"),
|
|
537
|
+
Type.Literal("incoming"),
|
|
538
|
+
Type.Literal("outgoing"),
|
|
539
|
+
], {
|
|
540
|
+
description: '"all" (default) — both; "incoming" — awaiting YOUR decision; ' +
|
|
541
|
+
'"outgoing" — awaiting the counterparty. Direction is derived ' +
|
|
542
|
+
"from each request's status; it is not a state you can filter " +
|
|
543
|
+
"the network by.",
|
|
456
544
|
})),
|
|
457
|
-
|
|
458
|
-
description:
|
|
545
|
+
include_history: Type.Optional(Type.Boolean({
|
|
546
|
+
description: "Include resolved requests (accepted / ignored / withdrawn). " +
|
|
547
|
+
"Default true — pass false for pending requests only.",
|
|
548
|
+
})),
|
|
549
|
+
counterparty: Type.Optional(Type.String({
|
|
550
|
+
description: "Handle of one agent — show the full per-pair history with " +
|
|
551
|
+
"@handle — bypasses the one-row-per-counterparty collapse.",
|
|
459
552
|
})),
|
|
460
553
|
}),
|
|
461
554
|
execute: withUpdateNotice(async (_id, params) => {
|
|
462
555
|
const cfg = requirePlatformConfig();
|
|
463
556
|
const apiKey = requireApiKey();
|
|
464
|
-
const
|
|
465
|
-
|
|
557
|
+
const rawDirection = params.direction;
|
|
558
|
+
if (rawDirection !== undefined &&
|
|
559
|
+
rawDirection !== "all" &&
|
|
560
|
+
rawDirection !== "incoming" &&
|
|
561
|
+
rawDirection !== "outgoing") {
|
|
562
|
+
return textResult(`${quoteRejected(rawDirection)} is not a direction — use "all", "incoming", or "outgoing". Nothing was read.`);
|
|
563
|
+
}
|
|
564
|
+
const rawHistory = params.include_history;
|
|
565
|
+
if (rawHistory !== undefined && typeof rawHistory !== "boolean") {
|
|
566
|
+
return textResult(`${quoteRejected(rawHistory)} is not a boolean — include_history takes true or false. Nothing was read.`);
|
|
567
|
+
}
|
|
568
|
+
const direction = rawDirection ?? "all";
|
|
569
|
+
const includeHistory = rawHistory ?? true;
|
|
570
|
+
const rawCounterparty = params.counterparty;
|
|
571
|
+
const counterparty = typeof rawCounterparty === "string"
|
|
572
|
+
? rawCounterparty.replace(/^@/, "").trim() || undefined
|
|
573
|
+
: undefined;
|
|
466
574
|
let result;
|
|
467
575
|
try {
|
|
468
576
|
result = await listRequests(cfg, apiKey, {
|
|
469
|
-
status,
|
|
470
|
-
direction,
|
|
471
577
|
limit: 100,
|
|
578
|
+
...(counterparty ? { counterparty } : {}),
|
|
472
579
|
});
|
|
473
580
|
}
|
|
474
581
|
catch (err) {
|
|
@@ -476,6 +583,9 @@ export function registerTools(api) {
|
|
|
476
583
|
if (err.status === 401) {
|
|
477
584
|
return textResult("Authentication failed. The runtime key may be invalid. Ask the user to run `openclaw channels login --channel agent-network`.");
|
|
478
585
|
}
|
|
586
|
+
if (err.status === 404) {
|
|
587
|
+
return textResult("This Services deployment does not serve the packaged connection-request endpoint. Nothing was read.");
|
|
588
|
+
}
|
|
479
589
|
return textResult(`Failed to list requests: ${err.message}`);
|
|
480
590
|
}
|
|
481
591
|
if (err instanceof PlatformNetworkError) {
|
|
@@ -483,53 +593,57 @@ export function registerTools(api) {
|
|
|
483
593
|
}
|
|
484
594
|
throw err;
|
|
485
595
|
}
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
: "incoming ";
|
|
492
|
-
return textResult(`No ${dirLabel}${status === "all" ? "" : `${status} `}connection requests.`);
|
|
493
|
-
}
|
|
494
|
-
const lines = result.items.map((item) => {
|
|
495
|
-
const dir = item.direction ?? direction;
|
|
496
|
-
const node = dir === "outgoing" ? item.toNode : item.fromNode;
|
|
497
|
-
const otherName = node?.displayName || node?.handle || "unknown";
|
|
498
|
-
const otherHandle = node?.handle || "unknown";
|
|
499
|
-
const kindLabel = formatKindLabel(node?.kind);
|
|
500
|
-
const dirArrow = dir === "outgoing" ? "→" : "←";
|
|
501
|
-
const parts = [
|
|
502
|
-
`• ${dirArrow} ${kindLabel}${otherName} (@${otherHandle})`,
|
|
503
|
-
` ID: ${item.id} | Variant: ${item.variant} | Status: ${item.status}`,
|
|
504
|
-
];
|
|
505
|
-
if (item.matchmaking?.title) {
|
|
506
|
-
parts.push(` Why: ${item.matchmaking.title}`);
|
|
507
|
-
}
|
|
508
|
-
if (item.matchmaking?.benefit) {
|
|
509
|
-
parts.push(` Benefit: ${item.matchmaking.benefit}`);
|
|
510
|
-
}
|
|
511
|
-
if (item.matchmaking?.scenario) {
|
|
512
|
-
parts.push(` Context: ${item.matchmaking.scenario}`);
|
|
513
|
-
}
|
|
514
|
-
return parts.join("\n");
|
|
596
|
+
const filtered = result.items.filter((item) => {
|
|
597
|
+
const label = directionLabel(item.status);
|
|
598
|
+
if (label === "history")
|
|
599
|
+
return includeHistory;
|
|
600
|
+
return direction === "all" || direction === label;
|
|
515
601
|
});
|
|
516
|
-
const
|
|
517
|
-
?
|
|
518
|
-
:
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
? "\n\nUse masons_accept_request or masons_decline_request with the ID to act on incoming requests."
|
|
602
|
+
const rows = counterparty
|
|
603
|
+
? filtered
|
|
604
|
+
: collapseToCounterpartyGrain(filtered);
|
|
605
|
+
const scope = direction !== "all" &&
|
|
606
|
+
rows.every((item) => directionLabel(item.status) === direction)
|
|
607
|
+
? `${direction} `
|
|
523
608
|
: "";
|
|
524
|
-
|
|
609
|
+
const withWho = counterparty ? ` with @${counterparty}` : "";
|
|
610
|
+
const moreLine = result.nextCursor !== null
|
|
611
|
+
? "\n\nMore requests exist beyond this page — this is the most recent page, not the whole history."
|
|
612
|
+
: "";
|
|
613
|
+
if (rows.length === 0) {
|
|
614
|
+
return textResult(`No ${scope}${includeHistory ? "" : "pending "}connection requests${withWho}.${moreLine}`);
|
|
615
|
+
}
|
|
616
|
+
const lines = rows.map((item) => {
|
|
617
|
+
const node = item.counterparty;
|
|
618
|
+
const label = directionLabel(item.status);
|
|
619
|
+
const arrow = label === "incoming" ? "←" : label === "outgoing" ? "→" : "·";
|
|
620
|
+
const kindLabel = formatKindLabel(node.kind);
|
|
621
|
+
const handle = node.handle ? `@${node.handle}` : "no handle";
|
|
622
|
+
const shadowMark = item.shadowed ? " | shadowed" : "";
|
|
623
|
+
return [
|
|
624
|
+
`• ${arrow} ${kindLabel}${counterpartyName(node)} (${handle})`,
|
|
625
|
+
` ID: ${item.id} | Status: ${item.status} | ${label}${shadowMark}`,
|
|
626
|
+
].join("\n");
|
|
627
|
+
});
|
|
628
|
+
const hints = [];
|
|
629
|
+
if (rows.some((item) => item.status === "awaiting-your-decision")) {
|
|
630
|
+
hints.push("Use masons_accept_request or masons_ignore_request with the ID to act on requests awaiting your decision.");
|
|
631
|
+
}
|
|
632
|
+
if (rows.some((item) => item.status === "awaiting-counterparty")) {
|
|
633
|
+
hints.push("Use masons_withdraw_request with the ID to take back a request you sent.");
|
|
634
|
+
}
|
|
635
|
+
const suffix = hints.length > 0 ? `\n\n${hints.join("\n")}` : "";
|
|
636
|
+
return textResult(`${rows.length} ${scope}request(s)${withWho}:\n\n${lines.join("\n\n")}${suffix}${moreLine}`);
|
|
525
637
|
}),
|
|
526
638
|
});
|
|
527
639
|
api.registerTool({
|
|
528
640
|
name: "masons_accept_request",
|
|
529
|
-
description: "Accept
|
|
641
|
+
description: "Accept a connection request awaiting your decision. Takes the ID " +
|
|
642
|
+
"masons_list_requests shows for that request; the whole request is " +
|
|
643
|
+
"resolved in one act.",
|
|
530
644
|
parameters: Type.Object({
|
|
531
645
|
requestId: Type.String({
|
|
532
|
-
description: "ID of the connection request to accept",
|
|
646
|
+
description: "ID of the connection request to accept, as shown by masons_list_requests",
|
|
533
647
|
}),
|
|
534
648
|
}),
|
|
535
649
|
execute: withUpdateNotice(async (_id, params) => {
|
|
@@ -543,15 +657,57 @@ export function registerTools(api) {
|
|
|
543
657
|
const requestId = params.requestId;
|
|
544
658
|
try {
|
|
545
659
|
const result = await acceptRequest(cfg, apiKey, requestId);
|
|
546
|
-
|
|
547
|
-
|
|
660
|
+
if (result.outcome === "already_resolved") {
|
|
661
|
+
return textResult(formatAlreadyResolved(result.status, "accept"));
|
|
662
|
+
}
|
|
663
|
+
const node = result.request?.counterparty;
|
|
664
|
+
const who = node ? counterpartyName(node) : "that Node";
|
|
548
665
|
const handle = node?.handle ? ` (@${node.handle})` : "";
|
|
549
666
|
const kindLabel = formatKindLabel(node?.kind);
|
|
667
|
+
if (result.idempotent) {
|
|
668
|
+
return textResult(`Already connected with ${kindLabel}${who}${handle} — you accepted this request earlier and nothing changed now. You can send messages using masons_send_message.`);
|
|
669
|
+
}
|
|
550
670
|
return textResult(`Connected with ${kindLabel}${who}${handle}! You can now send messages using masons_send_message.`);
|
|
551
671
|
}
|
|
552
672
|
catch (err) {
|
|
553
673
|
if (err instanceof PlatformApiError && err.status === 404) {
|
|
554
|
-
return textResult(
|
|
674
|
+
return textResult(formatVerb404(err, "accept"));
|
|
675
|
+
}
|
|
676
|
+
if (err instanceof PlatformNetworkError) {
|
|
677
|
+
return textResult(networkErrorText(err));
|
|
678
|
+
}
|
|
679
|
+
throw err;
|
|
680
|
+
}
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
api.registerTool({
|
|
684
|
+
name: "masons_ignore_request",
|
|
685
|
+
description: "Set aside a connection request awaiting your decision. " +
|
|
686
|
+
"This resolves the whole request on your side, silently: the sender is " +
|
|
687
|
+
"not notified and sees no change — there is no refusal they can observe.",
|
|
688
|
+
parameters: Type.Object({
|
|
689
|
+
requestId: Type.String({
|
|
690
|
+
description: "ID of the connection request to set aside, as shown by masons_list_requests",
|
|
691
|
+
}),
|
|
692
|
+
}),
|
|
693
|
+
execute: withUpdateNotice(async (_id, params) => {
|
|
694
|
+
const cfg = requirePlatformConfig();
|
|
695
|
+
const apiKey = requireApiKey();
|
|
696
|
+
const requestId = params.requestId;
|
|
697
|
+
try {
|
|
698
|
+
const result = await ignoreRequest(cfg, apiKey, requestId);
|
|
699
|
+
if (result.outcome === "already_resolved") {
|
|
700
|
+
return textResult(formatAlreadyResolved(result.status, "set aside"));
|
|
701
|
+
}
|
|
702
|
+
if (result.idempotent) {
|
|
703
|
+
return textResult("This request was already set aside earlier; nothing changed.");
|
|
704
|
+
}
|
|
705
|
+
return textResult("Request set aside. The whole request is resolved on your side. " +
|
|
706
|
+
"The sender is not notified and sees no change.");
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
if (err instanceof PlatformApiError && err.status === 404) {
|
|
710
|
+
return textResult(formatVerb404(err, "ignore"));
|
|
555
711
|
}
|
|
556
712
|
if (err instanceof PlatformNetworkError) {
|
|
557
713
|
return textResult(networkErrorText(err));
|
|
@@ -561,11 +717,14 @@ export function registerTools(api) {
|
|
|
561
717
|
}),
|
|
562
718
|
});
|
|
563
719
|
api.registerTool({
|
|
564
|
-
name: "
|
|
565
|
-
description: "
|
|
720
|
+
name: "masons_withdraw_request",
|
|
721
|
+
description: "Withdraw a connection request YOU sent — one awaiting the counterparty. " +
|
|
722
|
+
"(masons_accept_request and masons_ignore_request act on requests " +
|
|
723
|
+
"awaiting your decision.) Takes the ID masons_list_requests shows for " +
|
|
724
|
+
"that request; the whole request is resolved in one act.",
|
|
566
725
|
parameters: Type.Object({
|
|
567
726
|
requestId: Type.String({
|
|
568
|
-
description: "ID of the connection request to
|
|
727
|
+
description: "ID of the outgoing connection request to withdraw, as shown by masons_list_requests",
|
|
569
728
|
}),
|
|
570
729
|
}),
|
|
571
730
|
execute: withUpdateNotice(async (_id, params) => {
|
|
@@ -573,12 +732,21 @@ export function registerTools(api) {
|
|
|
573
732
|
const apiKey = requireApiKey();
|
|
574
733
|
const requestId = params.requestId;
|
|
575
734
|
try {
|
|
576
|
-
await
|
|
577
|
-
|
|
735
|
+
const result = await withdrawRequest(cfg, apiKey, requestId);
|
|
736
|
+
if (result.outcome === "already_resolved") {
|
|
737
|
+
return textResult(formatAlreadyResolved(result.status, "withdraw"));
|
|
738
|
+
}
|
|
739
|
+
if (result.idempotent) {
|
|
740
|
+
return textResult("This request was already withdrawn earlier; nothing changed.");
|
|
741
|
+
}
|
|
742
|
+
return textResult("Request withdrawn. A new request to the same agent can be sent later. " +
|
|
743
|
+
"If the recipient had not acted on it yet, the pending request " +
|
|
744
|
+
"disappears from their view; if they had already set it aside, " +
|
|
745
|
+
"nothing changes for them.");
|
|
578
746
|
}
|
|
579
747
|
catch (err) {
|
|
580
748
|
if (err instanceof PlatformApiError && err.status === 404) {
|
|
581
|
-
return textResult(
|
|
749
|
+
return textResult(formatVerb404(err, "withdraw"));
|
|
582
750
|
}
|
|
583
751
|
if (err instanceof PlatformNetworkError) {
|
|
584
752
|
return textResult(networkErrorText(err));
|
|
@@ -727,36 +895,50 @@ export function registerTools(api) {
|
|
|
727
895
|
execute: async () => {
|
|
728
896
|
const pendingNotes = ownerNotesQueue.size();
|
|
729
897
|
if (pendingNotes > 0) {
|
|
730
|
-
return textResult(`You have ${pendingNotes} pending note(s) for your owner that haven't been delivered yet. ` +
|
|
731
|
-
`Report them to your owner first, then call masons_upgrade again.`);
|
|
898
|
+
return textResult(withRoster(`You have ${pendingNotes} pending note(s) for your owner that haven't been delivered yet. ` +
|
|
899
|
+
`Report them to your owner first, then call masons_upgrade again.`));
|
|
732
900
|
}
|
|
733
901
|
const currentVersion = getPluginVersion();
|
|
734
902
|
const latestVersion = await getLatestPublishedVersion();
|
|
735
903
|
if (!latestVersion) {
|
|
736
|
-
return textResult("Could not check for updates (network issue). Try again later.");
|
|
904
|
+
return textResult(withRoster("Could not check for updates (network issue). Try again later."));
|
|
737
905
|
}
|
|
738
906
|
if (latestVersion === currentVersion) {
|
|
739
|
-
return textResult(`You're on the latest version (v${currentVersion}). No update needed.`);
|
|
907
|
+
return textResult(withRoster(`You're on the latest version (v${currentVersion}). No update needed.`));
|
|
740
908
|
}
|
|
741
909
|
if (!SEMVER_RE.test(latestVersion)) {
|
|
742
|
-
return textResult(`Registry returned an unexpected version format: "${latestVersion}". Try again later.`);
|
|
910
|
+
return textResult(withRoster(`Registry returned an unexpected version format: "${latestVersion}". Try again later.`));
|
|
743
911
|
}
|
|
744
912
|
const cmd = upgradeCmd(latestVersion);
|
|
745
|
-
return textResult([
|
|
913
|
+
return textResult(withRoster([
|
|
746
914
|
`Update available: v${currentVersion} → v${latestVersion}`,
|
|
747
915
|
"",
|
|
748
916
|
"Run this command using the exec tool:",
|
|
749
917
|
cmd,
|
|
750
918
|
"",
|
|
919
|
+
"After the install finishes and BEFORE restarting: read " +
|
|
920
|
+
"`contracts.tools` from the newly installed package's " +
|
|
921
|
+
"`openclaw.plugin.json` (its installed package directory under " +
|
|
922
|
+
"`~/.openclaw/`) and compare it with the roster below. Tell your " +
|
|
923
|
+
"owner about every tool the new version adds. If their " +
|
|
924
|
+
"`openclaw.json` allows plugin tools by individual name under " +
|
|
925
|
+
"`tools.alsoAllow`, the new names must be added there — or the " +
|
|
926
|
+
"entry replaced with `group:plugins` — or the new tools will be " +
|
|
927
|
+
"missing after the restart with no error reported.",
|
|
928
|
+
"",
|
|
751
929
|
"Then restart the gateway using the gateway tool.",
|
|
752
|
-
"If you cannot run commands, tell the user to run in Terminal
|
|
930
|
+
"If you cannot run commands, tell the user to run in Terminal — and " +
|
|
931
|
+
"compare the new package's `openclaw.plugin.json` with the roster " +
|
|
932
|
+
"below before that restart:",
|
|
753
933
|
`${cmd} && ${GATEWAY_RESTART_CMD}`,
|
|
754
|
-
].join("\n"));
|
|
934
|
+
].join("\n")));
|
|
755
935
|
},
|
|
756
936
|
});
|
|
757
937
|
api.registerTool({
|
|
758
938
|
name: "masons_link_identity",
|
|
759
939
|
description: "Link your owner's identity on the current channel to their Passport identity. " +
|
|
940
|
+
"Not to be confused with masons_link: that tool binds this Runtime to " +
|
|
941
|
+
"an Agent Node. This one aliases owner identities across channels. " +
|
|
760
942
|
"Provide only the current channel's entry in 'channel:peerId' format " +
|
|
761
943
|
"(e.g., 'telegram:5099353300', 'feishu:ou_abc123'). " +
|
|
762
944
|
"The canonical name and Passport entry are added automatically. " +
|