libp2p-mesh 2026.6.19 → 2026.6.21
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 +227 -5
- package/dist/src/agent-tools.d.ts +34 -14
- package/dist/src/agent-tools.js +99 -14
- package/dist/src/instance-peer-store.js +6 -2
- package/dist/src/instance-router.js +137 -21
- package/dist/src/labels-wizard.d.ts +21 -0
- package/dist/src/labels-wizard.js +168 -0
- package/dist/src/peer-label-store.d.ts +11 -0
- package/dist/src/peer-label-store.js +172 -0
- package/dist/src/plugin.js +13 -4
- package/dist/src/profile-cli.d.ts +9 -0
- package/dist/src/profile-cli.js +33 -0
- package/dist/src/prompt-config.js +35 -9
- package/dist/src/types.d.ts +44 -7
- package/dist/src/user-md-agent-attributes.d.ts +24 -0
- package/dist/src/user-md-agent-attributes.js +180 -0
- package/package.json +1 -1
- package/src/agent-tools.ts +144 -21
- package/src/instance-peer-store.ts +7 -2
- package/src/instance-router.ts +194 -24
- package/src/labels-wizard.ts +221 -0
- package/src/peer-label-store.ts +224 -0
- package/src/plugin.ts +14 -5
- package/src/profile-cli.ts +48 -0
- package/src/prompt-config.ts +35 -9
- package/src/types.ts +50 -5
- package/src/user-md-agent-attributes.ts +243 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { matchesUserAttribute, mergeUserPublicAttributes } from "./user-attributes.js";
|
|
1
|
+
import { matchesUserAttribute, mergeUserPublicAttributes, normalizeAttributeKey, normalizeAttributeValue, } from "./user-attributes.js";
|
|
2
2
|
const MAX_DELIVERY_CACHE_ENTRIES = 1000;
|
|
3
3
|
function parsePayload(msg) {
|
|
4
4
|
try {
|
|
@@ -34,6 +34,25 @@ function describeAttributeMatch(match) {
|
|
|
34
34
|
}
|
|
35
35
|
return `structured attribute ${match.key}=${match.value}`;
|
|
36
36
|
}
|
|
37
|
+
function effectiveAttributeMatchScope(scope) {
|
|
38
|
+
return scope ?? "public";
|
|
39
|
+
}
|
|
40
|
+
function matchesLocalPeerLabel(attribute, match) {
|
|
41
|
+
if (match.kind !== "structured") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (normalizeAttributeKey(attribute.key) === normalizeAttributeKey(match.key) &&
|
|
45
|
+
normalizeAttributeValue(attribute.value) === normalizeAttributeValue(match.value));
|
|
46
|
+
}
|
|
47
|
+
function buildUserAttributeTarget(record, matchedAttribute, matchSource) {
|
|
48
|
+
return {
|
|
49
|
+
instanceId: record.instanceId,
|
|
50
|
+
...(record.instanceName ? { instanceName: record.instanceName } : {}),
|
|
51
|
+
peerId: record.peerId,
|
|
52
|
+
matchedAttribute,
|
|
53
|
+
matchSource,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
37
56
|
function displayTargetId(target) {
|
|
38
57
|
const id = typeof target.id === "string" ? target.id.trim() : "";
|
|
39
58
|
return id && id.length > 0 ? id : undefined;
|
|
@@ -101,6 +120,8 @@ export function createInstanceRouter(options) {
|
|
|
101
120
|
const pendingAcks = new Map();
|
|
102
121
|
const deliveryCache = new Map();
|
|
103
122
|
const unsubs = [];
|
|
123
|
+
const pendingAttributePeers = new Set();
|
|
124
|
+
let attributeRefreshPromise;
|
|
104
125
|
function localInstanceId() {
|
|
105
126
|
const identity = mesh.getInstanceIdentity();
|
|
106
127
|
if (!identity) {
|
|
@@ -108,20 +129,24 @@ export function createInstanceRouter(options) {
|
|
|
108
129
|
}
|
|
109
130
|
return identity.id;
|
|
110
131
|
}
|
|
111
|
-
async function loadUserPublicAttributes() {
|
|
132
|
+
async function loadUserPublicAttributes(loadOptions) {
|
|
133
|
+
const userMdPromise = loadOptions?.refreshUserMd && options.userAttributeSource?.refreshTags
|
|
134
|
+
? options.userAttributeSource.refreshTags()
|
|
135
|
+
: options.userAttributeSource?.loadTags() ?? Promise.resolve([]);
|
|
136
|
+
const profilePromise = options.userProfileStore?.listAttributes().catch((error) => {
|
|
137
|
+
logger?.warn?.(`[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`);
|
|
138
|
+
return [];
|
|
139
|
+
}) ?? Promise.resolve([]);
|
|
112
140
|
const [userMdTags, profileAttributes] = await Promise.all([
|
|
113
|
-
|
|
141
|
+
userMdPromise.catch((error) => {
|
|
114
142
|
logger?.warn?.(`[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`);
|
|
115
143
|
return [];
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
logger?.warn?.(`[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`);
|
|
119
|
-
return [];
|
|
120
|
-
}) ?? Promise.resolve([]),
|
|
144
|
+
}),
|
|
145
|
+
profilePromise,
|
|
121
146
|
]);
|
|
122
147
|
return mergeUserPublicAttributes(userMdTags, profileAttributes);
|
|
123
148
|
}
|
|
124
|
-
|
|
149
|
+
function buildBaseAnnouncePayload() {
|
|
125
150
|
const identity = mesh.getInstanceIdentity();
|
|
126
151
|
if (!identity) {
|
|
127
152
|
throw new Error("Local instance identity is not initialized");
|
|
@@ -132,15 +157,87 @@ export function createInstanceRouter(options) {
|
|
|
132
157
|
instanceName: identity.name,
|
|
133
158
|
multiaddrs: mesh.getMultiaddrs(),
|
|
134
159
|
pubkey: identity.pubkey,
|
|
135
|
-
userPublicAttributes: await loadUserPublicAttributes(),
|
|
136
160
|
announcedAt: Date.now(),
|
|
137
161
|
};
|
|
138
162
|
}
|
|
163
|
+
async function buildAttributeAnnouncePayload() {
|
|
164
|
+
return {
|
|
165
|
+
...buildBaseAnnouncePayload(),
|
|
166
|
+
userPublicAttributes: await loadUserPublicAttributes({ refreshUserMd: true }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function attributeAnnouncePeers(extraPeerId) {
|
|
170
|
+
const peers = new Set();
|
|
171
|
+
for (const peerId of mesh.getConnectedPeers()) {
|
|
172
|
+
if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
|
|
173
|
+
peers.add(peerId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const peerId of announcedPeers) {
|
|
177
|
+
if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
|
|
178
|
+
peers.add(peerId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (extraPeerId && extraPeerId !== mesh.getLocalPeerId()) {
|
|
182
|
+
peers.add(extraPeerId);
|
|
183
|
+
}
|
|
184
|
+
return [...peers];
|
|
185
|
+
}
|
|
186
|
+
async function sendAttributeAnnounceToPeers(peers) {
|
|
187
|
+
if (peers.length === 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const payload = await buildAttributeAnnouncePayload();
|
|
191
|
+
for (const peerId of peers) {
|
|
192
|
+
try {
|
|
193
|
+
await mesh.sendStructuredMessage(peerId, {
|
|
194
|
+
id: crypto.randomUUID(),
|
|
195
|
+
type: "instance-announce",
|
|
196
|
+
to: peerId,
|
|
197
|
+
payload: JSON.stringify(payload),
|
|
198
|
+
});
|
|
199
|
+
logAnnounce("Sent", peerId, payload);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger?.warn?.(`[libp2p-mesh] Failed to send attribute announce to ${peerId}: ${summarizeError(error)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function queueAttributeRefresh(extraPeerId) {
|
|
207
|
+
const peers = extraPeerId === undefined
|
|
208
|
+
? attributeAnnouncePeers()
|
|
209
|
+
: isNonEmptyString(extraPeerId) && extraPeerId !== mesh.getLocalPeerId()
|
|
210
|
+
? [extraPeerId]
|
|
211
|
+
: [];
|
|
212
|
+
for (const peerId of peers) {
|
|
213
|
+
pendingAttributePeers.add(peerId);
|
|
214
|
+
}
|
|
215
|
+
if (pendingAttributePeers.size === 0 && !attributeRefreshPromise) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
drainAttributeRefresh();
|
|
219
|
+
}
|
|
220
|
+
function drainAttributeRefresh() {
|
|
221
|
+
if (!attributeRefreshPromise) {
|
|
222
|
+
attributeRefreshPromise = (async () => {
|
|
223
|
+
while (pendingAttributePeers.size > 0) {
|
|
224
|
+
const peers = [...pendingAttributePeers];
|
|
225
|
+
pendingAttributePeers.clear();
|
|
226
|
+
await sendAttributeAnnounceToPeers(peers).catch((error) => {
|
|
227
|
+
logger?.warn?.(`[libp2p-mesh] Failed to refresh public attributes: ${summarizeError(error)}`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
})().finally(() => {
|
|
231
|
+
attributeRefreshPromise = undefined;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return attributeRefreshPromise;
|
|
235
|
+
}
|
|
139
236
|
async function announceToPeer(peerId) {
|
|
140
237
|
if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
|
|
141
238
|
return;
|
|
142
239
|
}
|
|
143
|
-
const payload =
|
|
240
|
+
const payload = buildBaseAnnouncePayload();
|
|
144
241
|
await mesh.sendStructuredMessage(peerId, {
|
|
145
242
|
id: crypto.randomUUID(),
|
|
146
243
|
type: "instance-announce",
|
|
@@ -149,6 +246,13 @@ export function createInstanceRouter(options) {
|
|
|
149
246
|
});
|
|
150
247
|
announcedPeers.add(peerId);
|
|
151
248
|
logAnnounce("Sent", peerId, payload);
|
|
249
|
+
queueAttributeRefresh(peerId);
|
|
250
|
+
}
|
|
251
|
+
async function refreshPublicAttributes() {
|
|
252
|
+
for (const peerId of attributeAnnouncePeers()) {
|
|
253
|
+
pendingAttributePeers.add(peerId);
|
|
254
|
+
}
|
|
255
|
+
await drainAttributeRefresh();
|
|
152
256
|
}
|
|
153
257
|
function announceSummary(direction, peerId, payload, changed) {
|
|
154
258
|
const changedDetail = changed === undefined ? "" : ` changed=${changed}`;
|
|
@@ -475,27 +579,36 @@ export function createInstanceRouter(options) {
|
|
|
475
579
|
error: ack.error,
|
|
476
580
|
};
|
|
477
581
|
}
|
|
478
|
-
async function resolveUserAttributeTargets(match) {
|
|
582
|
+
async function resolveUserAttributeTargets(match, scope) {
|
|
479
583
|
const records = await store.list();
|
|
480
584
|
const targets = [];
|
|
481
585
|
for (const record of records) {
|
|
482
|
-
const
|
|
483
|
-
|
|
586
|
+
const publicAttribute = scope === "local"
|
|
587
|
+
? undefined
|
|
588
|
+
: record.userPublicAttributes?.find((attribute) => matchesUserAttribute(attribute, match));
|
|
589
|
+
const localAttribute = scope === "public"
|
|
590
|
+
? undefined
|
|
591
|
+
: (await options.peerLabelStore?.listLabels(record.instanceId))?.find((attribute) => matchesLocalPeerLabel(attribute, match));
|
|
592
|
+
if (publicAttribute && localAttribute && scope === "all") {
|
|
593
|
+
targets.push(buildUserAttributeTarget(record, publicAttribute, "all"));
|
|
484
594
|
continue;
|
|
485
595
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
596
|
+
if (publicAttribute) {
|
|
597
|
+
targets.push(buildUserAttributeTarget(record, publicAttribute, "public"));
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (localAttribute) {
|
|
601
|
+
targets.push(buildUserAttributeTarget(record, localAttribute, "local"));
|
|
602
|
+
}
|
|
492
603
|
}
|
|
493
604
|
return targets;
|
|
494
605
|
}
|
|
495
606
|
async function sendUserAttributeMessage(match, message, sendOptions = {}) {
|
|
496
|
-
const
|
|
607
|
+
const scope = effectiveAttributeMatchScope(sendOptions.scope);
|
|
608
|
+
const targets = await resolveUserAttributeTargets(match, scope);
|
|
497
609
|
if (targets.length === 0) {
|
|
498
610
|
return {
|
|
611
|
+
scope,
|
|
499
612
|
matched: 0,
|
|
500
613
|
sent: 0,
|
|
501
614
|
delivered: 0,
|
|
@@ -505,6 +618,7 @@ export function createInstanceRouter(options) {
|
|
|
505
618
|
}
|
|
506
619
|
if (sendOptions.dryRun === true) {
|
|
507
620
|
return {
|
|
621
|
+
scope,
|
|
508
622
|
matched: targets.length,
|
|
509
623
|
sent: 0,
|
|
510
624
|
delivered: 0,
|
|
@@ -538,6 +652,7 @@ export function createInstanceRouter(options) {
|
|
|
538
652
|
}
|
|
539
653
|
}
|
|
540
654
|
return {
|
|
655
|
+
scope,
|
|
541
656
|
matched: targets.length,
|
|
542
657
|
sent: results.filter((result) => result.sent).length,
|
|
543
658
|
delivered: results.filter((result) => result.delivered).length,
|
|
@@ -552,6 +667,7 @@ export function createInstanceRouter(options) {
|
|
|
552
667
|
stop,
|
|
553
668
|
handleMessage,
|
|
554
669
|
announceToPeer,
|
|
670
|
+
refreshPublicAttributes,
|
|
555
671
|
listInstances,
|
|
556
672
|
resolveInstance,
|
|
557
673
|
sendInstanceMessage,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SetupPrompter } from "./setup-wizard.js";
|
|
2
|
+
import type { InstancePeerRecord, LocalPeerLabel } from "./types.js";
|
|
3
|
+
export type PeerLabelsWriter = {
|
|
4
|
+
replaceLabels(instanceId: string, labels: LocalPeerLabel[]): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export type RunLabelsWizardOptions = {
|
|
7
|
+
prompter: SetupPrompter;
|
|
8
|
+
instances: InstancePeerRecord[];
|
|
9
|
+
getLabels(instanceId: string): Promise<LocalPeerLabel[]>;
|
|
10
|
+
writer: PeerLabelsWriter;
|
|
11
|
+
};
|
|
12
|
+
export type LabelsWizardResult = {
|
|
13
|
+
status: "saved";
|
|
14
|
+
instanceId: string;
|
|
15
|
+
labels: LocalPeerLabel[];
|
|
16
|
+
message: string;
|
|
17
|
+
} | {
|
|
18
|
+
status: "cancelled";
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function runLabelsWizard(options: RunLabelsWizardOptions): Promise<LabelsWizardResult>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SetupCancelledError } from "./setup-wizard.js";
|
|
2
|
+
import { normalizeAttributeKey } from "./user-attributes.js";
|
|
3
|
+
const CANCELLED_MESSAGE = "Local labels update cancelled. No changes were written.";
|
|
4
|
+
const SAVED_MESSAGE = "Local labels saved.";
|
|
5
|
+
export async function runLabelsWizard(options) {
|
|
6
|
+
try {
|
|
7
|
+
if (options.instances.length === 0) {
|
|
8
|
+
options.prompter.print("No discovered instances found. Start the gateway and wait for peer announcements first.");
|
|
9
|
+
return cancelledResult();
|
|
10
|
+
}
|
|
11
|
+
options.prompter.print(formatInstancesOverview(options.instances));
|
|
12
|
+
let selectedInstance = options.instances[await selectInstanceIndex(options.prompter, options.instances)];
|
|
13
|
+
let labels = normalizeLabels(await options.getLabels(selectedInstance.instanceId));
|
|
14
|
+
options.prompter.print(formatLabelsOverview(selectedInstance, labels));
|
|
15
|
+
while (true) {
|
|
16
|
+
const action = await options.prompter.select("What do you want to do?", [
|
|
17
|
+
{ label: "Add local label", value: "add-label" },
|
|
18
|
+
{ label: "Edit local label", value: "edit-label" },
|
|
19
|
+
{ label: "Remove local label", value: "remove-label" },
|
|
20
|
+
{ label: "Choose another instance", value: "choose-instance" },
|
|
21
|
+
{ label: "Save and finish", value: "save-finish" },
|
|
22
|
+
{ label: "Cancel", value: "cancel" },
|
|
23
|
+
]);
|
|
24
|
+
switch (action) {
|
|
25
|
+
case "add-label":
|
|
26
|
+
labels = normalizeLabels([...labels, await promptForLabel(options.prompter)]);
|
|
27
|
+
options.prompter.print(formatLabelsOverview(selectedInstance, labels));
|
|
28
|
+
break;
|
|
29
|
+
case "edit-label":
|
|
30
|
+
labels = await promptForLabelEdit(options.prompter, labels);
|
|
31
|
+
options.prompter.print(formatLabelsOverview(selectedInstance, labels));
|
|
32
|
+
break;
|
|
33
|
+
case "remove-label":
|
|
34
|
+
labels = await promptForLabelRemoval(options.prompter, labels);
|
|
35
|
+
options.prompter.print(formatLabelsOverview(selectedInstance, labels));
|
|
36
|
+
break;
|
|
37
|
+
case "choose-instance":
|
|
38
|
+
selectedInstance = options.instances[await selectInstanceIndex(options.prompter, options.instances)];
|
|
39
|
+
labels = normalizeLabels(await options.getLabels(selectedInstance.instanceId));
|
|
40
|
+
options.prompter.print(formatLabelsOverview(selectedInstance, labels));
|
|
41
|
+
break;
|
|
42
|
+
case "save-finish":
|
|
43
|
+
await options.writer.replaceLabels(selectedInstance.instanceId, labels);
|
|
44
|
+
return {
|
|
45
|
+
status: "saved",
|
|
46
|
+
instanceId: selectedInstance.instanceId,
|
|
47
|
+
labels,
|
|
48
|
+
message: SAVED_MESSAGE,
|
|
49
|
+
};
|
|
50
|
+
case "cancel":
|
|
51
|
+
return cancelledResult();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof SetupCancelledError) {
|
|
57
|
+
return cancelledResult();
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function promptForLabel(prompter) {
|
|
63
|
+
const category = await prompter.select("Label category", [
|
|
64
|
+
{ label: "Group", value: "group" },
|
|
65
|
+
{ label: "Project", value: "project" },
|
|
66
|
+
{ label: "Role", value: "role" },
|
|
67
|
+
{ label: "Skill", value: "skill" },
|
|
68
|
+
{ label: "Custom key", value: "custom" },
|
|
69
|
+
]);
|
|
70
|
+
const key = category === "custom"
|
|
71
|
+
? normalizeAttributeKey(await prompter.input("Custom key", { required: true }))
|
|
72
|
+
: category;
|
|
73
|
+
const value = await prompter.input("Label value", { required: true });
|
|
74
|
+
return { key, value: value.trim() };
|
|
75
|
+
}
|
|
76
|
+
async function promptForLabelEdit(prompter, labels) {
|
|
77
|
+
if (labels.length === 0) {
|
|
78
|
+
prompter.print("No local labels configured for this instance.");
|
|
79
|
+
return labels;
|
|
80
|
+
}
|
|
81
|
+
const selectedIndex = await selectLabelIndex(prompter, "Label to edit", labels);
|
|
82
|
+
const nextLabel = await promptForLabel(prompter);
|
|
83
|
+
return normalizeLabels(labels.map((label, index) => (index === selectedIndex ? nextLabel : label)));
|
|
84
|
+
}
|
|
85
|
+
async function promptForLabelRemoval(prompter, labels) {
|
|
86
|
+
if (labels.length === 0) {
|
|
87
|
+
prompter.print("No local labels configured for this instance.");
|
|
88
|
+
return labels;
|
|
89
|
+
}
|
|
90
|
+
const selectedIndex = await selectLabelIndex(prompter, "Label to remove", labels);
|
|
91
|
+
return labels.filter((_label, index) => index !== selectedIndex);
|
|
92
|
+
}
|
|
93
|
+
async function selectInstanceIndex(prompter, instances) {
|
|
94
|
+
const selectedKey = await prompter.select("Instance to label", instances.map((instance, index) => ({
|
|
95
|
+
label: formatInstance(instance),
|
|
96
|
+
value: `instance-index-${index}`,
|
|
97
|
+
})));
|
|
98
|
+
const match = /^instance-index-(\d+)$/.exec(selectedKey);
|
|
99
|
+
return match ? Number(match[1]) : 0;
|
|
100
|
+
}
|
|
101
|
+
async function selectLabelIndex(prompter, message, labels) {
|
|
102
|
+
const selectedKey = await prompter.select(message, labels.map((label, index) => ({
|
|
103
|
+
label: formatLabel(label),
|
|
104
|
+
value: `label-index-${index}`,
|
|
105
|
+
})));
|
|
106
|
+
const match = /^label-index-(\d+)$/.exec(selectedKey);
|
|
107
|
+
return match ? Number(match[1]) : -1;
|
|
108
|
+
}
|
|
109
|
+
function normalizeLabels(labels) {
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const normalized = [];
|
|
112
|
+
for (const label of labels) {
|
|
113
|
+
const key = normalizeAttributeKey(label.key);
|
|
114
|
+
const value = label.value.trim();
|
|
115
|
+
if (!key || !value) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const id = `${key}\0${value}`;
|
|
119
|
+
if (seen.has(id)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
seen.add(id);
|
|
123
|
+
normalized.push({ key, value });
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
function formatInstancesOverview(instances) {
|
|
128
|
+
return ["Discovered instances:", ...instances.map((instance, index) => ` ${index + 1}. ${formatInstance(instance)}`)].join("\n");
|
|
129
|
+
}
|
|
130
|
+
function formatLabelsOverview(instance, labels) {
|
|
131
|
+
return [
|
|
132
|
+
`Selected instance: ${formatInstance(instance)}`,
|
|
133
|
+
"",
|
|
134
|
+
"Local labels:",
|
|
135
|
+
...formatLabelList(labels),
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
function formatLabelList(labels) {
|
|
139
|
+
if (labels.length === 0) {
|
|
140
|
+
return [" none"];
|
|
141
|
+
}
|
|
142
|
+
return labels.map((label, index) => ` ${index + 1}. ${formatLabel(label)}`);
|
|
143
|
+
}
|
|
144
|
+
function formatInstance(instance) {
|
|
145
|
+
const name = instance.instanceName ? `${instance.instanceName} ` : "";
|
|
146
|
+
return `${name}${instance.instanceId} (${formatPublicAttributes(instance.userPublicAttributes ?? [])})`;
|
|
147
|
+
}
|
|
148
|
+
function formatPublicAttributes(attributes) {
|
|
149
|
+
if (attributes.length === 0) {
|
|
150
|
+
return "public attributes: none";
|
|
151
|
+
}
|
|
152
|
+
return `public attributes: ${attributes.map(formatPublicAttribute).join(", ")}`;
|
|
153
|
+
}
|
|
154
|
+
function formatPublicAttribute(attribute) {
|
|
155
|
+
if (attribute.kind === "tag") {
|
|
156
|
+
return attribute.label;
|
|
157
|
+
}
|
|
158
|
+
return `${attribute.key}: ${attribute.value}`;
|
|
159
|
+
}
|
|
160
|
+
function formatLabel(label) {
|
|
161
|
+
return `${label.key}: ${label.value}`;
|
|
162
|
+
}
|
|
163
|
+
function cancelledResult() {
|
|
164
|
+
return {
|
|
165
|
+
status: "cancelled",
|
|
166
|
+
message: CANCELLED_MESSAGE,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PeerLabelStore } from "./types.js";
|
|
2
|
+
type PeerLabelStoreLogger = {
|
|
3
|
+
debug?: (message: string) => void;
|
|
4
|
+
warn?: (message: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function resolvePeerLabelsPath(customPath?: string): string;
|
|
7
|
+
export declare function createPeerLabelStore(options?: {
|
|
8
|
+
path?: string;
|
|
9
|
+
logger?: PeerLabelStoreLogger;
|
|
10
|
+
}): PeerLabelStore;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeAttributeKey, normalizeAttributeValue, } from "./user-attributes.js";
|
|
5
|
+
export function resolvePeerLabelsPath(customPath) {
|
|
6
|
+
if (customPath)
|
|
7
|
+
return customPath;
|
|
8
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
9
|
+
if (stateDir) {
|
|
10
|
+
return path.join(stateDir, "libp2p", "peer-labels.json");
|
|
11
|
+
}
|
|
12
|
+
return path.join(homedir(), ".openclaw", "libp2p", "peer-labels.json");
|
|
13
|
+
}
|
|
14
|
+
function emptyLabelsFile() {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
updatedAt: Date.now(),
|
|
18
|
+
peers: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null;
|
|
23
|
+
}
|
|
24
|
+
function trimmedString(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
30
|
+
}
|
|
31
|
+
function getLocalPeerLabelId(label) {
|
|
32
|
+
return `${normalizeAttributeKey(label.key)}:${normalizeAttributeValue(label.value)}`;
|
|
33
|
+
}
|
|
34
|
+
function normalizePeerLabel(value) {
|
|
35
|
+
if (!isRecord(value)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const key = trimmedString(value.key);
|
|
39
|
+
const labelValue = trimmedString(value.value);
|
|
40
|
+
if (!key || !labelValue) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
key: normalizeAttributeKey(key),
|
|
45
|
+
value: labelValue,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function normalizePeerLabels(labels) {
|
|
49
|
+
if (!Array.isArray(labels)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const normalized = [];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
for (const value of labels) {
|
|
55
|
+
const label = normalizePeerLabel(value);
|
|
56
|
+
if (!label) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const id = getLocalPeerLabelId(label);
|
|
60
|
+
if (seen.has(id)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
seen.add(id);
|
|
64
|
+
normalized.push(label);
|
|
65
|
+
}
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
function normalizePeerLabelsFile(value) {
|
|
69
|
+
const candidate = isRecord(value) ? value : {};
|
|
70
|
+
const peers = {};
|
|
71
|
+
const candidatePeers = isRecord(candidate.peers) ? candidate.peers : {};
|
|
72
|
+
for (const [instanceId, peerValue] of Object.entries(candidatePeers)) {
|
|
73
|
+
const peer = isRecord(peerValue) ? peerValue : {};
|
|
74
|
+
const labels = normalizePeerLabels(peer.labels);
|
|
75
|
+
if (labels.length === 0) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
peers[instanceId] = { labels };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
updatedAt: typeof candidate.updatedAt === "number" ? candidate.updatedAt : Date.now(),
|
|
83
|
+
peers,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function createPeerLabelStore(options) {
|
|
87
|
+
const filePath = resolvePeerLabelsPath(options?.path);
|
|
88
|
+
const logger = options?.logger;
|
|
89
|
+
let cached;
|
|
90
|
+
let mutationQueue = Promise.resolve();
|
|
91
|
+
async function load() {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(filePath, "utf8");
|
|
94
|
+
cached = normalizePeerLabelsFile(JSON.parse(raw));
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const code = error.code;
|
|
99
|
+
if (code === "ENOENT") {
|
|
100
|
+
cached = emptyLabelsFile();
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
const backupPath = `${filePath}.corrupt-${Date.now()}`;
|
|
104
|
+
try {
|
|
105
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
106
|
+
await rename(filePath, backupPath);
|
|
107
|
+
logger?.warn?.(`[libp2p-mesh] Peer label store unreadable; moved to ${backupPath}`);
|
|
108
|
+
}
|
|
109
|
+
catch (renameError) {
|
|
110
|
+
logger?.warn?.(`[libp2p-mesh] Peer label store unreadable; failed to move corrupt file to ${backupPath}: ${renameError.message}`);
|
|
111
|
+
}
|
|
112
|
+
cached = emptyLabelsFile();
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function save(file) {
|
|
117
|
+
const nextFile = {
|
|
118
|
+
version: 1,
|
|
119
|
+
updatedAt: Date.now(),
|
|
120
|
+
peers: normalizePeerLabelsFile(file).peers,
|
|
121
|
+
};
|
|
122
|
+
const dir = path.dirname(filePath);
|
|
123
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
124
|
+
await mkdir(dir, { recursive: true });
|
|
125
|
+
await writeFile(tmpPath, `${JSON.stringify(nextFile, null, 2)}\n`, "utf8");
|
|
126
|
+
await rename(tmpPath, filePath);
|
|
127
|
+
cached = nextFile;
|
|
128
|
+
logger?.debug?.(`[libp2p-mesh] Saved peer label store to ${filePath}`);
|
|
129
|
+
return nextFile;
|
|
130
|
+
}
|
|
131
|
+
async function runMutation(fn) {
|
|
132
|
+
const next = mutationQueue.then(fn, fn);
|
|
133
|
+
mutationQueue = next.then(() => undefined, () => undefined);
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
async function listRawLabels(instanceId) {
|
|
137
|
+
const file = await load();
|
|
138
|
+
return [...(file.peers[instanceId]?.labels ?? [])];
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
load,
|
|
142
|
+
save,
|
|
143
|
+
listRawLabels,
|
|
144
|
+
async listLabels(instanceId) {
|
|
145
|
+
const labels = await listRawLabels(instanceId);
|
|
146
|
+
return labels.map((label) => ({
|
|
147
|
+
kind: "structured",
|
|
148
|
+
key: label.key,
|
|
149
|
+
value: label.value,
|
|
150
|
+
label: label.value,
|
|
151
|
+
source: "local",
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
async replaceLabels(instanceId, labels) {
|
|
155
|
+
return runMutation(async () => {
|
|
156
|
+
const file = await load();
|
|
157
|
+
const peers = { ...file.peers };
|
|
158
|
+
const normalizedLabels = normalizePeerLabels(labels);
|
|
159
|
+
if (normalizedLabels.length === 0) {
|
|
160
|
+
delete peers[instanceId];
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
peers[instanceId] = { labels: normalizedLabels };
|
|
164
|
+
}
|
|
165
|
+
return save({
|
|
166
|
+
...file,
|
|
167
|
+
peers,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -4,7 +4,8 @@ import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
|
|
|
4
4
|
import { createInstancePeerStore } from "./instance-peer-store.js";
|
|
5
5
|
import { createInstanceRouter } from "./instance-router.js";
|
|
6
6
|
import { createMeshNetwork } from "./mesh.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createPeerLabelStore } from "./peer-label-store.js";
|
|
8
|
+
import { createUserMdAgentAttributeSource } from "./user-md-agent-attributes.js";
|
|
8
9
|
import { createUserProfileStore } from "./user-profile-store.js";
|
|
9
10
|
import { buildP2PTools } from "./agent-tools.js";
|
|
10
11
|
import { registerLibp2pMeshCli } from "./profile-cli.js";
|
|
@@ -12,7 +13,6 @@ export function registerLibp2pMesh(api) {
|
|
|
12
13
|
registerLibp2pMeshWithDeps(api);
|
|
13
14
|
}
|
|
14
15
|
export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
15
|
-
registerLibp2pMeshCli(api);
|
|
16
16
|
const config = api.pluginConfig;
|
|
17
17
|
let unsubscribeInbound;
|
|
18
18
|
let serviceStarted = false;
|
|
@@ -22,7 +22,8 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
22
22
|
logger: api.logger,
|
|
23
23
|
});
|
|
24
24
|
const store = createInstancePeerStore({ logger: api.logger });
|
|
25
|
-
const
|
|
25
|
+
const peerLabelStore = createPeerLabelStore({ logger: api.logger });
|
|
26
|
+
const userAttributeSource = createUserMdAgentAttributeSource({ logger: api.logger });
|
|
26
27
|
const userProfileStore = createUserProfileStore({ logger: api.logger });
|
|
27
28
|
const delivery = createOpenClawRuntimeInboundDelivery({
|
|
28
29
|
config: api.config,
|
|
@@ -44,6 +45,14 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
44
45
|
logger: api.logger,
|
|
45
46
|
userAttributeSource,
|
|
46
47
|
userProfileStore,
|
|
48
|
+
peerLabelStore,
|
|
49
|
+
});
|
|
50
|
+
registerLibp2pMeshCli(api, {
|
|
51
|
+
profile: {
|
|
52
|
+
async afterProfileSave() {
|
|
53
|
+
await router.refreshPublicAttributes();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
47
56
|
});
|
|
48
57
|
const channel = createLibp2pMeshChannel(mesh);
|
|
49
58
|
function attachInboundHandlers() {
|
|
@@ -162,7 +171,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
162
171
|
plugin: channel,
|
|
163
172
|
});
|
|
164
173
|
// 3. Register Agent Tools
|
|
165
|
-
const tools = buildP2PTools(mesh, router);
|
|
174
|
+
const tools = buildP2PTools(mesh, router, { peerLabelStore });
|
|
166
175
|
for (const tool of tools) {
|
|
167
176
|
api.registerTool(tool);
|
|
168
177
|
}
|