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.
@@ -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
- options.userAttributeSource?.loadTags().catch((error) => {
141
+ userMdPromise.catch((error) => {
114
142
  logger?.warn?.(`[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`);
115
143
  return [];
116
- }) ?? Promise.resolve([]),
117
- options.userProfileStore?.listAttributes().catch((error) => {
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
- async function buildAnnouncePayload() {
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 = await buildAnnouncePayload();
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 matchedAttribute = record.userPublicAttributes?.find((attribute) => matchesUserAttribute(attribute, match));
483
- if (!matchedAttribute) {
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
- targets.push({
487
- instanceId: record.instanceId,
488
- instanceName: record.instanceName,
489
- peerId: record.peerId,
490
- matchedAttribute,
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 targets = await resolveUserAttributeTargets(match);
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
+ }
@@ -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 { createUserMdAttributeSource } from "./user-md-attributes.js";
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 userAttributeSource = createUserMdAttributeSource({ logger: api.logger });
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
  }