@masons/agent-network 0.4.19 → 0.4.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/dist/channel.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { AgentNetworkAccount } from "./config-schema.js";
2
+ /** Get the stored owner Passport address (first is_owner visit). */
3
+ export declare function getOwnerPassportAddress(): string | null;
2
4
  interface ChannelMeta {
3
5
  name: string;
4
6
  description: string;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAuB9D,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AA+ND,eAAO,MAAM,mBAAmB,EAAE,yBAqNjC,CAAC"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA2B9D,oEAAoE;AACpE,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAQD,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AA4PD,eAAO,MAAM,mBAAmB,EAAE,yBAwNjC,CAAC"}
package/dist/channel.js CHANGED
@@ -1,14 +1,26 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import createDebug from "debug";
3
- import { clearConnectorClient, clearConversationManager, extractNetworkConfig, initConnectorClient, initConversationManager, initToolConfig, requirePluginRuntime, } from "./config.js";
3
+ import { clearConnectorClient, clearConversationManager, extractNetworkConfig, getDmScope, initConnectorClient, initConversationManager, initToolConfig, isIdentityLinksConfigured, requirePluginRuntime, } from "./config.js";
4
4
  import { ConnectorClient } from "./connector-client.js";
5
5
  import { ConversationManager } from "./conversation-manager.js";
6
6
  import { clearEnvironmentContext } from "./environment-context.js";
7
7
  import { extractHandleFromAddress } from "./handle-utils.js";
8
+ import { ownerNotesQueue } from "./owner-notes.js";
8
9
  import { clearOwnerState, isOwnerAddress, markOwnerAddress, setCurrentTurnIsOwner, } from "./owner-session-state.js";
9
10
  import { setCurrentTurnSender } from "./turn-context.js";
10
11
  import { checkForUpdate } from "./update-check.js";
11
12
  const dbg = createDebug("agent-network:channel");
13
+ // Cross-channel identity notification: once per startAccount lifecycle.
14
+ // Reset on startAccount(). isIdentityLinksConfigured() provides the
15
+ // cross-restart idempotency (checks config file).
16
+ let notificationAttempted = false;
17
+ // Owner's Passport address: recorded on first is_owner MESSAGE_RECEIVED.
18
+ // Used by masons_link_identity tool to auto-include the Passport entry.
19
+ let storedOwnerPassportAddress = null;
20
+ /** Get the stored owner Passport address (first is_owner visit). */
21
+ export function getOwnerPassportAddress() {
22
+ return storedOwnerPassportAddress;
23
+ }
12
24
  // --- Sender identity ---
13
25
  /**
14
26
  * Extract a human-readable name from the sender's metadata.
@@ -81,6 +93,30 @@ async function handleAddressedMessage(event, conversationManager, client, ctx) {
81
93
  // Connector includes is_owner in metadata for Passport visitors.
82
94
  if (event.metadata?.is_owner === true) {
83
95
  markOwnerAddress(fromAddress);
96
+ // Record Passport address for tool auto-include (first visit only)
97
+ if (!storedOwnerPassportAddress) {
98
+ storedOwnerPassportAddress = fromAddress;
99
+ dbg("recorded owner Passport address: %s", fromAddress);
100
+ }
101
+ // --- dmScope-aware notification (2026-03-26, #986 redesign) ---
102
+ // Notify the owner about cross-channel identity linking availability.
103
+ // Notification is:
104
+ // - Skipped in "main" mode (linking has no effect — Gateway skips resolveLinkedPeerId)
105
+ // - Skipped if identityLinks already configured (already linked)
106
+ // - Sent once per startAccount lifecycle (in-memory idempotency)
107
+ // Content is channel-agnostic and language-neutral (semantic event, not identity claim).
108
+ if (getDmScope() !== "main" &&
109
+ !isIdentityLinksConfigured() &&
110
+ !notificationAttempted) {
111
+ notificationAttempted = true;
112
+ ownerNotesQueue.enqueue({
113
+ content: "Owner visited via Passport (verified). " +
114
+ "Cross-channel identity linking is available but not configured. " +
115
+ "The owner can link their identity across channels so conversations share context.",
116
+ timestamp: Date.now(),
117
+ });
118
+ dbg("queued identity linking notification (dmScope=%s)", getDmScope());
119
+ }
84
120
  }
85
121
  setCurrentTurnIsOwner(isOwnerAddress(fromAddress));
86
122
  // Resolve contact from address. Register inbound conversation if new.
@@ -246,6 +282,9 @@ export const agentNetworkChannel = {
246
282
  gateway: {
247
283
  async startAccount(ctx) {
248
284
  dbg("starting account %s", ctx.accountId);
285
+ // Reset per-session state
286
+ notificationAttempted = false;
287
+ storedOwnerPassportAddress = null;
249
288
  // Defensive: clear stale singletons from a previous cycle.
250
289
  // The abort handler intentionally leaves them alive for in-flight tool
251
290
  // calls (#935). Clean up here at cycle start so initConnectorClient()
package/dist/config.d.ts CHANGED
@@ -38,15 +38,22 @@ export declare function extractNetworkConfig(cfg: Record<string, unknown>): Reco
38
38
  * Ensures tools always read the Host's current config values.
39
39
  */
40
40
  export declare function initToolConfig(cfg: Record<string, unknown>): void;
41
- /** Whether session.identityLinks has an agent-network entry. */
41
+ /**
42
+ * Whether `session.identityLinks` contains at least one entry with the
43
+ * `agent-network:` channel prefix — indicating the owner's agent-network
44
+ * identity is linked to another channel.
45
+ */
42
46
  export declare function isIdentityLinksConfigured(): boolean;
43
47
  /**
44
- * Mark the identity link hint as shown for this session.
45
- * Prevents repeated injection on every owner turn.
48
+ * Current dmScope mode from Gateway config (`cfg.session.dmScope`).
49
+ *
50
+ * Determines how the plugin handles cross-channel identity:
51
+ * - "main" (default): all DMs share one session. Identity linking has no effect.
52
+ * - "per-peer": identity linking merges sessions across channels.
53
+ * - "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
54
+ * peerId but sessions stay separate per channel.
46
55
  */
47
- export declare function markIdentityLinkHintShown(): void;
48
- /** Whether the identity link hint has been shown this session. */
49
- export declare function isIdentityLinkHintShown(): boolean;
56
+ export declare function getDmScope(): string;
50
57
  /**
51
58
  * Inject the live ConnectorClient after WebSocket connection is established.
52
59
  *
@@ -195,8 +202,13 @@ export declare function markProfileComplete(): Promise<void>;
195
202
  /**
196
203
  * Write cross-channel identity links to `config.session.identityLinks`.
197
204
  *
198
- * Merges a new canonical identity group into the existing identityLinks
199
- * map. Never overwrites other canonical groups.
205
+ * **Entry-level additive merge** (2026-03-26, #986 redesign):
206
+ * New entries are merged INTO the existing canonical group, deduped,
207
+ * lowercase-normalized. Never overwrites other canonical groups, and
208
+ * never removes existing entries within the same group.
209
+ *
210
+ * This allows one-channel-at-a-time linking: first call adds Telegram,
211
+ * second call adds Feishu — without losing the Telegram entry.
200
212
  *
201
213
  * This writes to `session.*` (Gateway-level), not `channels.agent-network.*`.
202
214
  * Justified because cross-channel identity is inherently a Gateway concern,
@@ -206,6 +218,13 @@ export declare function markProfileComplete(): Promise<void>;
206
218
  * writeCredentials() and writeTargetHandle().
207
219
  */
208
220
  export declare function writeIdentityLinks(canonical: string, entries: string[]): Promise<void>;
221
+ /**
222
+ * Remove a canonical identity group from `config.session.identityLinks`.
223
+ *
224
+ * Undo path for `writeIdentityLinks()`. Removes only the specified
225
+ * canonical key; other groups are preserved.
226
+ */
227
+ export declare function removeIdentityLinks(canonical: string): Promise<void>;
209
228
  /** Pending state detected from config file — used for restart continuity. */
210
229
  export interface PendingState {
211
230
  /** Whether valid credentials exist (connectorUrl + token) */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -33,7 +33,12 @@ let storedPendingTarget = null;
33
33
  let storedNeedsProfile = false;
34
34
  // Cross-channel identity state (from cfg.session — set by initToolConfig)
35
35
  let storedIdentityLinksConfigured = false;
36
- let identityLinkHintShown = false;
36
+ // dmScope from cfg.session.dmScope — determines notification + linking behavior.
37
+ // "main" (default): all DMs share one session. Identity linking has no effect.
38
+ // "per-peer": identity linking merges sessions across channels.
39
+ // "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
40
+ // peerId but sessions stay separate per channel.
41
+ let storedDmScope = "main";
37
42
  // State cache generation — incremented when config-modifying functions write
38
43
  // to disk (writeCredentials, clearTargetHandle, markProfileNeeded, markProfileComplete).
39
44
  // before_prompt_build compares its cached generation to this value; mismatch
@@ -99,11 +104,20 @@ export function initToolConfig(cfg) {
99
104
  : null;
100
105
  storedNeedsProfile = networkCfg?.needsProfile === true;
101
106
  // --- Cross-channel identity state (cfg.session, not cfg.channels) ---
102
- // Read Gateway-level session.identityLinks to detect if the owner's
103
- // agent-network identity is linked to other channels. Used by
104
- // before_prompt_build to inject a one-time configuration hint.
107
+ // Decision: 2026-03-26, during cross-channel identity linking design (#969, #986).
108
+ //
109
+ // Reads Gateway-level session config to detect:
110
+ // 1. Whether identityLinks already contains agent-network entries
111
+ // 2. The dmScope mode (determines notification + linking behavior)
112
+ //
113
+ // These are NOT agent-network-specific — any channel plugin would need
114
+ // similar capabilities. They live here because only one channel plugin
115
+ // exists today. When OpenClaw provides native cross-channel identity
116
+ // management, this should migrate.
105
117
  storedIdentityLinksConfigured = hasAgentNetworkIdentityLink(cfg);
106
- identityLinkHintShown = false; // Reset on startup / hot-reload
118
+ const sessionCfg = cfg.session;
119
+ storedDmScope =
120
+ typeof sessionCfg?.dmScope === "string" ? sessionCfg.dmScope : "main";
107
121
  }
108
122
  // ---------------------------------------------------------------------------
109
123
  // Cross-channel identity detection
@@ -132,20 +146,25 @@ function hasAgentNetworkIdentityLink(cfg) {
132
146
  }
133
147
  return false;
134
148
  }
135
- /** Whether session.identityLinks has an agent-network entry. */
149
+ /**
150
+ * Whether `session.identityLinks` contains at least one entry with the
151
+ * `agent-network:` channel prefix — indicating the owner's agent-network
152
+ * identity is linked to another channel.
153
+ */
136
154
  export function isIdentityLinksConfigured() {
137
155
  return storedIdentityLinksConfigured;
138
156
  }
139
157
  /**
140
- * Mark the identity link hint as shown for this session.
141
- * Prevents repeated injection on every owner turn.
158
+ * Current dmScope mode from Gateway config (`cfg.session.dmScope`).
159
+ *
160
+ * Determines how the plugin handles cross-channel identity:
161
+ * - "main" (default): all DMs share one session. Identity linking has no effect.
162
+ * - "per-peer": identity linking merges sessions across channels.
163
+ * - "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
164
+ * peerId but sessions stay separate per channel.
142
165
  */
143
- export function markIdentityLinkHintShown() {
144
- identityLinkHintShown = true;
145
- }
146
- /** Whether the identity link hint has been shown this session. */
147
- export function isIdentityLinkHintShown() {
148
- return identityLinkHintShown;
166
+ export function getDmScope() {
167
+ return storedDmScope;
149
168
  }
150
169
  // ---------------------------------------------------------------------------
151
170
  // ConnectorClient injection (called by startAccount() after connect)
@@ -455,8 +474,13 @@ function ensureSessionSection(config) {
455
474
  /**
456
475
  * Write cross-channel identity links to `config.session.identityLinks`.
457
476
  *
458
- * Merges a new canonical identity group into the existing identityLinks
459
- * map. Never overwrites other canonical groups.
477
+ * **Entry-level additive merge** (2026-03-26, #986 redesign):
478
+ * New entries are merged INTO the existing canonical group, deduped,
479
+ * lowercase-normalized. Never overwrites other canonical groups, and
480
+ * never removes existing entries within the same group.
481
+ *
482
+ * This allows one-channel-at-a-time linking: first call adds Telegram,
483
+ * second call adds Feishu — without losing the Telegram entry.
460
484
  *
461
485
  * This writes to `session.*` (Gateway-level), not `channels.agent-network.*`.
462
486
  * Justified because cross-channel identity is inherently a Gateway concern,
@@ -472,13 +496,43 @@ export async function writeIdentityLinks(canonical, entries) {
472
496
  const existing = typeof session.identityLinks === "object" && session.identityLinks !== null
473
497
  ? session.identityLinks
474
498
  : {};
475
- existing[canonical] = entries;
499
+ // Entry-level additive merge: combine new entries into existing group
500
+ const current = existing[canonical] ?? [];
501
+ const normalized = entries.map((e) => e.toLowerCase());
502
+ const merged = [...new Set([...current, ...normalized])];
503
+ existing[canonical] = merged;
476
504
  session.identityLinks = existing;
477
505
  await persistConfig(config);
478
506
  // Update module-level state — same exception pattern as writeCredentials
479
507
  storedIdentityLinksConfigured = true;
480
508
  stateCacheGeneration++;
481
509
  }
510
+ /**
511
+ * Remove a canonical identity group from `config.session.identityLinks`.
512
+ *
513
+ * Undo path for `writeIdentityLinks()`. Removes only the specified
514
+ * canonical key; other groups are preserved.
515
+ */
516
+ export async function removeIdentityLinks(canonical) {
517
+ const config = await readConfig();
518
+ const session = ensureSessionSection(config);
519
+ const existing = typeof session.identityLinks === "object" && session.identityLinks !== null
520
+ ? session.identityLinks
521
+ : {};
522
+ delete existing[canonical];
523
+ // If identityLinks is now empty, remove the key entirely
524
+ if (Object.keys(existing).length === 0) {
525
+ delete session.identityLinks;
526
+ }
527
+ else {
528
+ session.identityLinks = existing;
529
+ }
530
+ await persistConfig(config);
531
+ // Re-check if any agent-network links remain
532
+ storedIdentityLinksConfigured = Object.values(existing).some((entries) => Array.isArray(entries) &&
533
+ entries.some((e) => typeof e === "string" && e.startsWith("agent-network:")));
534
+ stateCacheGeneration++;
535
+ }
482
536
  /**
483
537
  * Detect pending state by reading config file directly.
484
538
  *
@@ -515,7 +569,7 @@ export function _resetForTesting() {
515
569
  storedPendingTarget = null;
516
570
  storedNeedsProfile = false;
517
571
  storedIdentityLinksConfigured = false;
518
- identityLinkHintShown = false;
572
+ storedDmScope = "main";
519
573
  storedConnectorClient = null;
520
574
  storedConversationManager = null;
521
575
  storedPluginRuntime = null;
@@ -36,7 +36,7 @@ export declare function setEnvironmentContext(agent?: {
36
36
  }): void;
37
37
  export declare function getAgentIdentity(): AgentIdentity | null;
38
38
  export declare function getOwnerIdentity(): OwnerIdentity | null;
39
- /** Convenience: get the owner's handle for interaction tagging. */
39
+ /** Convenience: get the owner's handle (used by auto-link in channel.ts). */
40
40
  export declare function getOwnerHandle(): string | null;
41
41
  /** Clear all environment context (connection lost — identity invalid). */
42
42
  export declare function clearEnvironmentContext(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"environment-context.d.ts","sourceRoot":"","sources":["../src/environment-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,KAAK,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,IAAI,CAkBN;AAMD,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAEvD;AAED,mEAAmE;AACnE,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAMD,0EAA0E;AAC1E,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC"}
1
+ {"version":3,"file":"environment-context.d.ts","sourceRoot":"","sources":["../src/environment-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,KAAK,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,IAAI,CAkBN;AAMD,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAEvD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAMD,0EAA0E;AAC1E,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC"}
@@ -59,7 +59,7 @@ export function getAgentIdentity() {
59
59
  export function getOwnerIdentity() {
60
60
  return ownerIdentity;
61
61
  }
62
- /** Convenience: get the owner's handle for interaction tagging. */
62
+ /** Convenience: get the owner's handle (used by auto-link in channel.ts). */
63
63
  export function getOwnerHandle() {
64
64
  return ownerIdentity?.handle ?? null;
65
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAiIA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;kBA4BI,iBAAiB;CAoWhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA0HA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;kBA4BI,iBAAiB;CAsWhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/plugin.js CHANGED
@@ -3,13 +3,13 @@
3
3
  // NOT imported by index.ts to avoid pulling ws/typebox into Next.js app bundles.
4
4
  import { agentNetworkChannel } from "./channel.js";
5
5
  import { configureInteractive } from "./cli-setup.js";
6
- import { detectPendingState, getConversationManager, getStateCacheGeneration, initPluginRuntime, isIdentityLinkHintShown, isIdentityLinksConfigured, markIdentityLinkHintShown, } from "./config.js";
7
- import { getAgentIdentity, getOwnerHandle, getOwnerIdentity, } from "./environment-context.js";
6
+ import { detectPendingState, getConversationManager, getDmScope, getStateCacheGeneration, initPluginRuntime, } from "./config.js";
7
+ import { getAgentIdentity } from "./environment-context.js";
8
8
  import { ownerNotesQueue } from "./owner-notes.js";
9
9
  import { consumeCurrentTurnIsOwner } from "./owner-session-state.js";
10
10
  import { sentMessageBuffer } from "./sent-message-buffer.js";
11
11
  import { registerTools } from "./tools.js";
12
- import { consumeCurrentTurnSender } from "./turn-context.js";
12
+ import { consumeCurrentTurnSender, setCurrentTurnChannelId, setCurrentTurnIsOwnerForTools, } from "./turn-context.js";
13
13
  import { getUpdateInfo } from "./update-check.js";
14
14
  import { PLUGIN_VERSION } from "./version.js";
15
15
  // ---------------------------------------------------------------------------
@@ -44,7 +44,7 @@ const MAX_INTERACTIONS = 5;
44
44
  *
45
45
  * Returns undefined if no interactions exist or CM is not available.
46
46
  */
47
- function buildInteractionContext(channelId, turnSender, ownerHandle) {
47
+ function buildInteractionContext(channelId, turnSender) {
48
48
  const cm = getConversationManager();
49
49
  if (!cm)
50
50
  return undefined;
@@ -61,12 +61,9 @@ function buildInteractionContext(channelId, turnSender, ownerHandle) {
61
61
  : "[Active interactions]";
62
62
  const lines = sorted.map((c) => {
63
63
  const who = `@${c.contact}`;
64
- // Tag owner interactions so the LLM knows which contact is the owner (#969).
65
- const isOwner = !!ownerHandle && c.contact.toLowerCase() === ownerHandle.toLowerCase();
66
- const ownerTag = isOwner ? " (your owner)" : "";
67
64
  const initiated = c.initiatedBy === "local" ? "you initiated" : "they initiated";
68
65
  const when = formatTimeAgo(c.lastMessageAt);
69
- return `• ${who}${ownerTag} — ${initiated} — last activity ${when}`;
66
+ return `• ${who} — ${initiated} — last activity ${when}`;
70
67
  });
71
68
  const isNetworkTurn = channelId === "agent-network";
72
69
  if (isNetworkTurn && turnSender) {
@@ -199,10 +196,30 @@ const plugin = {
199
196
  "should you report to your owner via masons_note_for_owner? " +
200
197
  "Should you follow up with another agent via masons_send_message? " +
201
198
  "When the interaction is complete, call masons_end_conversation as your FINAL action — " +
202
- "do not generate any text reply after ending.";
199
+ "do not generate any text reply after ending. " +
200
+ // --- Cross-channel identity linking knowledge removed from static TOOL_CONTEXT ---
201
+ // Moved to dynamicContext in before_prompt_build (#986 redesign).
202
+ // Reason: TOOL_CONTEXT is built in register() before startAccount() runs,
203
+ // so getDmScope() is not available. Linking knowledge is dmScope-conditional.
204
+ "";
203
205
  api.on("before_prompt_build", async (_event, ctx) => {
204
206
  const hookCtx = ctx;
205
207
  const channelId = hookCtx?.channelId;
208
+ // --- Consume per-turn flags unconditionally (#836, #873) ---
209
+ // MUST happen BEFORE any early return (config read timeout, etc.)
210
+ // to prevent stale state from leaking across turns. If not consumed,
211
+ // the value persists and is read on the NEXT turn, producing incorrect
212
+ // context or — critically — bypassing the access gate on identity tools.
213
+ const turnIsOwner = consumeCurrentTurnIsOwner();
214
+ const turnSender = consumeCurrentTurnSender();
215
+ // --- Store turn context for tool access gates (#986) ---
216
+ // Tools run AFTER before_prompt_build. The consumed flags above are gone
217
+ // by tool execution time. Store non-consuming copies so tools can check
218
+ // channelId and isOwner for access control (e.g., identity linking gate).
219
+ // These persist until the next before_prompt_build overwrites them.
220
+ // Assumption: OpenClaw processes turns sequentially per agent.
221
+ setCurrentTurnChannelId(channelId ?? null);
222
+ setCurrentTurnIsOwnerForTools(turnIsOwner);
206
223
  // --- Detect setup state (cached, re-read when config changes) ---
207
224
  const currentGeneration = getStateCacheGeneration();
208
225
  if (!cachedState || cachedGeneration !== currentGeneration) {
@@ -217,16 +234,11 @@ const plugin = {
217
234
  catch {
218
235
  // Config read failure — proceed without state-specific context.
219
236
  // Will retry on next turn (cachedState stays null).
237
+ // Per-turn flags were already consumed above — no stale state leak.
220
238
  return {};
221
239
  }
222
240
  }
223
241
  const state = cachedState;
224
- // --- Consume per-turn flags unconditionally (#836, #873) ---
225
- // Must consume on EVERY turn to prevent stale state from leaking across
226
- // turns. If not consumed (e.g., agent is in setup flow), the value would
227
- // persist and be read on the NEXT turn, producing incorrect context.
228
- const turnIsOwner = consumeCurrentTurnIsOwner();
229
- const turnSender = consumeCurrentTurnSender();
230
242
  // --- Build dynamic context based on setup state ---
231
243
  let dynamicContext;
232
244
  if (!state.hasCredentials) {
@@ -302,7 +314,7 @@ const plugin = {
302
314
  "your owner (Principal) will NOT see it. To report something to your owner, " +
303
315
  "call masons_note_for_owner.";
304
316
  // Append interaction space snapshot if active interactions exist.
305
- const interactionCtx = buildInteractionContext(channelId, turnSender, getOwnerHandle());
317
+ const interactionCtx = buildInteractionContext(channelId, turnSender);
306
318
  if (interactionCtx) {
307
319
  dynamicContext += `\n\n${interactionCtx}`;
308
320
  }
@@ -346,7 +358,7 @@ const plugin = {
346
358
  }
347
359
  // Append interaction space on owner turns too (#873).
348
360
  // Gives the agent awareness of active network interactions.
349
- const interactionCtx = buildInteractionContext(channelId, null, getOwnerHandle());
361
+ const interactionCtx = buildInteractionContext(channelId, null);
350
362
  if (interactionCtx) {
351
363
  dynamicContext = dynamicContext
352
364
  ? `${dynamicContext}\n\n${interactionCtx}`
@@ -354,49 +366,42 @@ const plugin = {
354
366
  }
355
367
  }
356
368
  }
357
- // --- Identity preamble (#969) ---
358
- // Inject agent + owner identity into dynamic context so the LLM knows
359
- // who it is and who its owner is on the network. Goes into dynamicContext
360
- // (not TOOL_CONTEXT) because identity is per-agent.
369
+ // --- dmScope-conditional linking knowledge (#986 redesign) ---
370
+ // Placed in dynamicContext (not static TOOL_CONTEXT) because getDmScope()
371
+ // is only available after startAccount() runs (initToolConfig reads cfg).
372
+ // TOOL_CONTEXT is built in register() before startAccount().
373
+ if (state.hasCredentials && !state.needsProfile && !state.pendingTarget) {
374
+ const dmScope = getDmScope();
375
+ if (dmScope !== "main") {
376
+ const linkingKnowledge = "[Cross-Channel Identity] When your owner visits your Passport page, " +
377
+ "the plugin detects them via verified authentication. " +
378
+ "You can link their identity across channels so conversations share context. " +
379
+ "Use masons_link_identity to link or masons_unlink_identity to undo. " +
380
+ (dmScope === "per-peer"
381
+ ? "After linking and gateway restart, conversations will merge across channels."
382
+ : "After linking and gateway restart, identity will be normalized but conversations stay separate under current session configuration.");
383
+ dynamicContext = dynamicContext
384
+ ? `${dynamicContext}\n\n${linkingKnowledge}`
385
+ : linkingKnowledge;
386
+ }
387
+ }
388
+ // --- Self-identity preamble (#969) ---
389
+ // Inject agent self-identity so the LLM knows who it is on the network.
390
+ // Owner identity is NOT injected here — agent rejected it as untrusted
391
+ // (0.4.18/0.4.19 case study). Owner recognition is handled via
392
+ // session.identityLinks at the Gateway config layer instead.
361
393
  if (state.hasCredentials) {
362
394
  const envAgent = getAgentIdentity();
363
- const envOwner = getOwnerIdentity();
364
395
  if (envAgent) {
365
396
  let identityLine = `[Identity] You are @${envAgent.handle}`;
366
397
  if (envAgent.name)
367
398
  identityLine += ` (${envAgent.name})`;
368
399
  identityLine += " on the agent network.";
369
- if (envOwner) {
370
- identityLine += ` Your owner is @${envOwner.handle}`;
371
- if (envOwner.displayName) {
372
- identityLine += ` (${envOwner.displayName})`;
373
- }
374
- identityLine += ".";
375
- }
376
400
  dynamicContext = dynamicContext
377
401
  ? `${identityLine}\n\n${dynamicContext}`
378
402
  : identityLine;
379
403
  }
380
404
  }
381
- // --- Cross-channel identity link hint (one-time per session) ---
382
- // When the owner's network handle is known but identityLinks is not
383
- // configured, inject a hint on the first owner turn so the SKILL can
384
- // guide the agent through linking. Only fires once per startup.
385
- const ownerHandleForLink = getOwnerHandle();
386
- if (state.hasCredentials &&
387
- channelId !== "agent-network" &&
388
- !isIdentityLinksConfigured() &&
389
- !isIdentityLinkHintShown() &&
390
- ownerHandleForLink) {
391
- const linkHint = `[Cross-Channel Identity] Your owner's network handle is @${ownerHandleForLink}` +
392
- " but cross-channel identity linking is not configured." +
393
- " This means your owner appears as a different person on each channel." +
394
- " Check the agent-network skill for setup instructions.";
395
- dynamicContext = dynamicContext
396
- ? `${dynamicContext}\n\n${linkHint}`
397
- : linkHint;
398
- markIdentityLinkHintShown();
399
- }
400
405
  // Post-upgrade verification: if an update is still available after
401
406
  // a gateway restart, the previous upgrade attempt may have failed.
402
407
  const updateInfo = getUpdateInfo();
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA8CH,UAAU,WAAW;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CACP,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;CAC3B;AAED,UAAU,OAAO;IACf,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AA0CD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAsFD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAq3BhD"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqDH,UAAU,WAAW;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CACP,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;CAC3B;AAED,UAAU,OAAO;IACf,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AA0CD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAsFD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA88BhD"}
package/dist/tools.js CHANGED
@@ -19,11 +19,13 @@
19
19
  */
20
20
  import { tmpdir } from "node:os";
21
21
  import { Type } from "@sinclair/typebox";
22
- import { clearTargetHandle, getOpenClawHome, getPendingTarget, isProfileNeeded, markProfileComplete, markProfileNeeded, requireApiKey, requireConversationManager, requirePlatformConfig, writeCredentials, writeIdentityLinks, } from "./config.js";
22
+ import { getOwnerPassportAddress } from "./channel.js";
23
+ import { clearTargetHandle, getDmScope, getOpenClawHome, getPendingTarget, isProfileNeeded, markProfileComplete, markProfileNeeded, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeCredentials, writeIdentityLinks, } from "./config.js";
23
24
  import { getOwnerHandle } from "./environment-context.js";
24
25
  import { ownerNotesQueue } from "./owner-notes.js";
25
26
  import { acceptRequest, declineRequest, getConnectionStatus, initSetup, listConnections, listRequests, onboard, PlatformApiError, pollSetup, reconnect, requestConnection, SetupExpiredError, SetupPendingError, updateProfile, } from "./platform-client.js";
26
27
  import { sentMessageBuffer } from "./sent-message-buffer.js";
28
+ import { getCurrentTurnChannelId, getCurrentTurnIsOwnerNonConsuming, } from "./turn-context.js";
27
29
  import { fetchLatestVersion, getPluginVersion, getUpdateInfo, } from "./update-check.js";
28
30
  // ---------------------------------------------------------------------------
29
31
  // Constants
@@ -751,49 +753,109 @@ export function registerTools(api) {
751
753
  },
752
754
  });
753
755
  // --- masons_link_identity -------------------------------------------------
756
+ // Simplified interface (#986 redesign): LLM provides only the current
757
+ // channel's entry. Canonical is auto-generated from getOwnerHandle().
758
+ // Passport entry is auto-included from storedOwnerPassportAddress.
759
+ // Access gate: deterministic owner-only check prevents prompt injection.
754
760
  api.registerTool({
755
761
  name: "masons_link_identity",
756
- description: "Link your owner's identity across channels (e.g., Telegram + agent network) so they are recognized as the same person. Writes to session.identityLinks in the OpenClaw config. Requires a gateway restart to take effect.",
762
+ description: "Link your owner's identity on the current channel to their Passport identity. " +
763
+ "Provide only the current channel's entry in 'channel:peerId' format " +
764
+ "(e.g., 'telegram:5099353300', 'feishu:ou_abc123'). " +
765
+ "The canonical name and Passport entry are added automatically. " +
766
+ "Requires a gateway restart to take effect.",
757
767
  parameters: Type.Object({
758
- telegramUserId: Type.String({
759
- description: "The owner's numeric Telegram user ID (e.g., '5099353300').",
768
+ entry: Type.String({
769
+ description: "Current channel's identity entry in 'channel:peerId' format " +
770
+ "(e.g., 'telegram:5099353300', 'feishu:ou_abc123').",
760
771
  }),
761
772
  }),
762
- execute: async (_id, params) => {
773
+ execute: withUpdateNotice(async (_id, params) => {
774
+ // --- Access gate (deterministic) ---
775
+ // Only the owner on an agent-network turn can invoke this tool.
776
+ // Prevents prompt injection from visitors or remote agents.
777
+ const hookChannelId = getCurrentTurnChannelId();
778
+ const turnIsOwner = getCurrentTurnIsOwnerNonConsuming();
779
+ if (hookChannelId === "agent-network" && !turnIsOwner) {
780
+ return textResult("Identity linking is only available to the agent's owner.");
781
+ }
763
782
  const ownerHandle = getOwnerHandle();
764
783
  if (!ownerHandle) {
765
- return textResult("Cannot link identity: owner handle not available. " +
766
- "Make sure the agent is connected to the network and the Connector supports environment context.");
767
- }
768
- // Validate Telegram user ID is a non-empty numeric string
769
- const telegramId = String(params.telegramUserId ?? "").trim();
770
- if (!telegramId || !/^\d{1,20}$/.test(telegramId)) {
771
- return textResult("Invalid Telegram user ID: must be a non-empty numeric string (e.g., '5099353300').");
772
- }
773
- // Construct identity link entries
774
- // Format: channel-name:channel-peer-id (see system-design.md §5)
775
- const entries = [
776
- `telegram:${telegramId}`,
777
- `agent-network:passport:@${ownerHandle}`,
778
- ];
784
+ return textResult("Not connected to the network. Cannot determine canonical identity.");
785
+ }
786
+ const entry = String(params.entry ?? "")
787
+ .trim()
788
+ .toLowerCase();
789
+ // Validate channel:peerId format — split on first colon only,
790
+ // both parts must be non-empty (e.g., "telegram:5099353300").
791
+ const colonIdx = entry.indexOf(":");
792
+ if (colonIdx < 1 || colonIdx === entry.length - 1) {
793
+ return textResult("Invalid format. Use 'channel:peerId' (e.g., 'telegram:5099353300').");
794
+ }
795
+ // Build entries: user-provided entry + auto-include Passport entry
796
+ const entries = [entry];
797
+ const passportAddr = getOwnerPassportAddress();
798
+ if (passportAddr) {
799
+ entries.push(`agent-network:${passportAddr}`);
800
+ }
779
801
  try {
802
+ // writeIdentityLinks does entry-level additive merge
780
803
  await writeIdentityLinks(ownerHandle, entries);
781
804
  }
782
805
  catch (err) {
783
806
  console.error("[masons_link_identity] writeIdentityLinks failed:", err);
784
807
  return textResult("Failed to write identity links to config. Try again.");
785
808
  }
809
+ const dmScope = getDmScope();
810
+ const effectExplanation = dmScope === "per-peer"
811
+ ? "After restart, conversations will merge across linked channels."
812
+ : "After restart, identity will be normalized. Conversations stay separate under current session configuration (per-channel-peer). For full merge, change session.dmScope to per-peer.";
786
813
  return textResult([
787
- `Identity linked successfully.`,
814
+ "Identity linked successfully.",
788
815
  `Canonical: ${ownerHandle}`,
789
- `Entries: ${entries.join(", ")}`,
816
+ `Entries added: ${entries.join(", ")}`,
817
+ "",
818
+ effectExplanation,
790
819
  "",
791
820
  "Restart required — call the `gateway` tool with action: restart, " +
792
821
  'reason: "Activate cross-channel identity linking".',
793
822
  ].join("\n"));
794
- },
823
+ }),
795
824
  },
796
825
  // optional: not all Gateway versions support this tool.
797
- // SKILL.md Pre-check tells the agent to verify tool availability.
798
826
  { optional: true });
827
+ // --- masons_unlink_identity -----------------------------------------------
828
+ // Undo path: removes the owner's canonical identity group from session.identityLinks.
829
+ // Access gate: same owner-only check as masons_link_identity.
830
+ api.registerTool({
831
+ name: "masons_unlink_identity",
832
+ description: "Remove your owner's cross-channel identity link. Reverses masons_link_identity. " +
833
+ "Requires a gateway restart to take effect.",
834
+ parameters: Type.Object({}),
835
+ execute: withUpdateNotice(async () => {
836
+ // --- Access gate (deterministic) ---
837
+ const hookChannelId = getCurrentTurnChannelId();
838
+ const turnIsOwner = getCurrentTurnIsOwnerNonConsuming();
839
+ if (hookChannelId === "agent-network" && !turnIsOwner) {
840
+ return textResult("Identity unlinking is only available to the agent's owner.");
841
+ }
842
+ const ownerHandle = getOwnerHandle();
843
+ if (!ownerHandle) {
844
+ return textResult("Not connected to the network. Cannot determine canonical identity.");
845
+ }
846
+ try {
847
+ await removeIdentityLinks(ownerHandle);
848
+ }
849
+ catch (err) {
850
+ console.error("[masons_unlink_identity] removeIdentityLinks failed:", err);
851
+ return textResult("Failed to remove identity link from config. Try again.");
852
+ }
853
+ return textResult([
854
+ `Identity link for "${ownerHandle}" removed.`,
855
+ "",
856
+ "Restart required — call the `gateway` tool with action: restart, " +
857
+ 'reason: "Remove cross-channel identity linking".',
858
+ ].join("\n"));
859
+ }),
860
+ }, { optional: true });
799
861
  }
@@ -22,9 +22,31 @@
22
22
  * ConversationManager to resolve sessionId → contact handle.
23
23
  */
24
24
  export declare function setCurrentTurnSender(contact: string | null): void;
25
+ /**
26
+ * Set the channel ID for the current turn.
27
+ * Called in before_prompt_build — the hook provides channelId.
28
+ * Persists through tool execution within the same turn.
29
+ */
30
+ export declare function setCurrentTurnChannelId(channelId: string | null): void;
31
+ /**
32
+ * Set the owner flag for the current turn (non-consuming copy).
33
+ * Called in before_prompt_build after consuming from owner-session-state.
34
+ * This copy persists through tool execution within the same turn.
35
+ */
36
+ export declare function setCurrentTurnIsOwnerForTools(value: boolean): void;
25
37
  /**
26
38
  * Consume the per-turn sender identity. Returns the value and resets to null.
27
39
  * Consuming prevents stale state from leaking into subsequent turns.
28
40
  */
29
41
  export declare function consumeCurrentTurnSender(): string | null;
42
+ /**
43
+ * Get the channel ID for the current turn (non-consuming).
44
+ * Used by tool access gates to determine which channel triggered the turn.
45
+ */
46
+ export declare function getCurrentTurnChannelId(): string | null;
47
+ /**
48
+ * Get the owner flag for the current turn (non-consuming).
49
+ * Used by tool access gates to enforce owner-only tools.
50
+ */
51
+ export declare function getCurrentTurnIsOwnerNonConsuming(): boolean;
30
52
  //# sourceMappingURL=turn-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjE;AAMD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAIxD"}
1
+ {"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmBH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElE;AAMD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAIxD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D"}
@@ -21,6 +21,10 @@
21
21
  // ---------------------------------------------------------------------------
22
22
  /** Contact handle of the sender for the current turn. null = unknown/unset. */
23
23
  let currentTurnSender = null;
24
+ /** Channel ID for the current turn. null = unknown/unset. */
25
+ let currentTurnChannelId = null;
26
+ /** Whether the current turn was triggered by the owner. */
27
+ let currentTurnIsOwner = false;
24
28
  // ---------------------------------------------------------------------------
25
29
  // Write API (called from channel.ts)
26
30
  // ---------------------------------------------------------------------------
@@ -32,6 +36,22 @@ let currentTurnSender = null;
32
36
  export function setCurrentTurnSender(contact) {
33
37
  currentTurnSender = contact;
34
38
  }
39
+ /**
40
+ * Set the channel ID for the current turn.
41
+ * Called in before_prompt_build — the hook provides channelId.
42
+ * Persists through tool execution within the same turn.
43
+ */
44
+ export function setCurrentTurnChannelId(channelId) {
45
+ currentTurnChannelId = channelId;
46
+ }
47
+ /**
48
+ * Set the owner flag for the current turn (non-consuming copy).
49
+ * Called in before_prompt_build after consuming from owner-session-state.
50
+ * This copy persists through tool execution within the same turn.
51
+ */
52
+ export function setCurrentTurnIsOwnerForTools(value) {
53
+ currentTurnIsOwner = value;
54
+ }
35
55
  // ---------------------------------------------------------------------------
36
56
  // Read API (called from plugin.ts)
37
57
  // ---------------------------------------------------------------------------
@@ -44,3 +64,17 @@ export function consumeCurrentTurnSender() {
44
64
  currentTurnSender = null;
45
65
  return value;
46
66
  }
67
+ /**
68
+ * Get the channel ID for the current turn (non-consuming).
69
+ * Used by tool access gates to determine which channel triggered the turn.
70
+ */
71
+ export function getCurrentTurnChannelId() {
72
+ return currentTurnChannelId;
73
+ }
74
+ /**
75
+ * Get the owner flag for the current turn (non-consuming).
76
+ * Used by tool access gates to enforce owner-only tools.
77
+ */
78
+ export function getCurrentTurnIsOwnerNonConsuming() {
79
+ return currentTurnIsOwner;
80
+ }
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Plugin version — must match package.json. Validated by prepublishOnly. */
2
- export declare const PLUGIN_VERSION = "0.4.19";
2
+ export declare const PLUGIN_VERSION = "0.4.21";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Plugin version — must match package.json. Validated by prepublishOnly. */
2
- export const PLUGIN_VERSION = "0.4.19";
2
+ export const PLUGIN_VERSION = "0.4.21";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masons/agent-network",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "description": "MASONS plugin for OpenClaw — connect your agent to the agent network",
5
5
  "license": "MIT",
6
6
  "author": "MASONS.ai <hello@masons.ai> (https://masons.ai)",
@@ -19,6 +19,15 @@
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc --watch",
25
+ "test": "tsc -p test/tsconfig.json && node --test --loader ts-node/esm test/**/*.test.ts",
26
+ "lint": "biome check",
27
+ "format": "biome format --write",
28
+ "prepublishOnly": "bash scripts/check-version.sh && npm run build && npm run test",
29
+ "release": "pnpm publish --access public"
30
+ },
22
31
  "files": [
23
32
  "dist/",
24
33
  "openclaw.plugin.json",
@@ -68,13 +77,5 @@
68
77
  "@types/ws": "^8",
69
78
  "ts-node": "^10",
70
79
  "typescript": "^5"
71
- },
72
- "scripts": {
73
- "build": "tsc",
74
- "dev": "tsc --watch",
75
- "test": "tsc -p test/tsconfig.json && node --test --loader ts-node/esm test/**/*.test.ts",
76
- "lint": "biome check",
77
- "format": "biome format --write",
78
- "release": "pnpm publish --access public"
79
80
  }
80
- }
81
+ }
@@ -52,7 +52,7 @@ Check your current state and go to the right section:
52
52
  - **Setup complete + user asks "who am I connected to" or wants to see connections** → Call `masons_list_connections` and show the results
53
53
  - **Connected + message from the network** → Go to **Network Behavior**
54
54
  - **Setup complete + general communication** → Go to **Network Behavior**
55
- - **Setup complete + [Cross-Channel Identity] hint in context** → Go to **Cross-Channel Identity**
55
+ - **User asks about cross-channel identity or why they appear as different people on different channels** → Go to **Cross-Channel Identity**
56
56
  - **Already connected, no pending actions** → You're ready. Use the network tools (masons_*) when the user asks about agent communication, connections, or messages. No action needed until then.
57
57
  - **Update available** (tool output mentions an update) → Go to **Upgrade** below
58
58
  - **User mentions upgrade or update** → Go to **Upgrade** below
@@ -166,46 +166,55 @@ After Step 3, the tool will confirm all three fields are filled and clear `needs
166
166
 
167
167
  ## Cross-Channel Identity
168
168
 
169
- One-time configuration to link your owner's identity across channels (e.g., Telegram + agent network). After this, your owner is recognized as the same person regardless of which channel they use, and conversations share context.
169
+ Reference section use when:
170
+ - Your owner asks about cross-channel identity or why they appear as a different person on different channels
171
+ - You received a notification about the owner visiting Passport and identity linking being available
172
+ - Your owner asks to link or unlink identities
170
173
 
171
- This section is triggered by the `[Cross-Channel Identity]` hint in your context. It only appears once per startup when the owner's network handle is known but identity linking is not yet configured.
174
+ **Background**: When the owner visits the Passport page, the plugin detects them via verified authentication (crypto, not guessing). If identity linking is not yet configured and the session mode allows it, you receive a notification on your owner's next turn. You can then guide the owner through linking their identity on the current channel.
172
175
 
173
- ### Step 1: Get Telegram user ID
176
+ **How it works**: Each channel has a different peer ID for the same person. Identity linking maps these IDs to a single canonical name, so the gateway treats them as one person. The canonical name and Passport entry are set automatically — the owner only needs to provide the current channel's ID.
174
177
 
175
- **Say to user:** "To recognize you across channels, I need your Telegram user ID — a numeric ID like `5099353300`. You can find it by messaging `@userinfobot` on Telegram."
178
+ ### Acknowledging the notification
176
179
 
177
- **Then:** Wait for the user to provide their Telegram user ID.
180
+ When your context includes a notification about the owner visiting Passport:
178
181
 
179
- If your owner declines or says they don't want this, respect their decision and move on. The hint will not appear again until the next restart.
182
+ **Say to owner:** Explain that you detected their visit to your web page, and offer to link their identities so conversations share context across channels. Ask for their user ID on this channel (e.g., Telegram numeric ID).
180
183
 
181
- ### Step 2: Confirm
184
+ ### Linking identities
182
185
 
183
- **Say to user:** "I'll link your identities so I recognize you across channels:
184
- - Telegram: `{telegram_id}`
185
- - Agent Network: `passport:@{handle}`
186
+ **Pre-check:** If `masons_link_identity` is not in your tool list, STOP. The plugin may be outdated. Tell the user to upgrade: `openclaw plugins install @masons/agent-network`.
186
187
 
187
- This means our conversations on Telegram and the agent network will share context. OK?"
188
+ **Then:** Once the owner provides their channel ID, call `masons_link_identity` with:
189
+ - `entry`: `"channel:peerId"` (e.g., `"telegram:5099353300"`, `"feishu:ou_abc123"`)
188
190
 
189
- **Then:** Wait for confirmation.
191
+ The canonical name and Passport entry are added automatically. One channel at a time — if the owner uses multiple channels, repeat for each.
190
192
 
191
- ### Step 3: Link
193
+ **Say to owner:** Relay the tool output (it explains the expected effect based on session configuration).
192
194
 
193
- **Pre-check:** If `masons_link_identity` is not in your tool list, STOP. Tell the user the plugin version may be outdated and suggest upgrading.
195
+ **Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Activate cross-channel identity linking"`, `note: "Cross-channel identity linked."`
194
196
 
195
- **Then:** Call `masons_link_identity` with the Telegram user ID.
197
+ **Fallback:** If the `gateway` tool is not available or fails:
198
+ **Say to owner:** "I couldn't restart automatically. Please run in your terminal: `openclaw gateway restart`"
199
+
200
+ ### Unlinking identities
201
+
202
+ **Say to owner:** "I'll remove the cross-channel link. After a restart, conversations on each channel will be separate again."
196
203
 
197
- **Say to user:** "Identity linked! I need to restart for this to take effect."
204
+ **Then:** Call `masons_unlink_identity` (no parameters needed it uses the canonical name automatically).
198
205
 
199
- **Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Activate cross-channel identity linking"`, `note: "Cross-channel identity linked. Your owner is now recognized across Telegram and the agent network."`
206
+ **Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Remove cross-channel identity linking"`, `note: "Cross-channel identity unlinked."`
200
207
 
201
208
  **Fallback:** If the `gateway` tool is not available or fails:
202
- **Say to user:** "I couldn't restart automatically. Please run in your terminal: `openclaw gateway restart`"
209
+ **Say to owner:** "I couldn't restart automatically. Please run in your terminal: `openclaw gateway restart`"
203
210
 
204
211
  ### Errors
205
212
 
206
- - If `masons_link_identity` returns an error about invalid Telegram ID, ask the user to double-check the ID (must be numeric).
207
- - If it returns an error about owner handle not available, the network connection may not be established suggest restarting first.
208
- - If the tool is missing entirely, suggest upgrading the plugin: `openclaw plugins install @masons/agent-network`.
213
+ - If `masons_link_identity` returns an error about invalid format, each entry must be `channel:peerId` (e.g., `telegram:5099353300`). Telegram IDs must be numeric.
214
+ - If the owner doesn't know their channel user ID, suggest ways to find it (e.g., messaging `@userinfobot` on Telegram for Telegram IDs).
215
+ - If the tool returns a write failure, suggest trying again.
216
+ - If the tool says identity linking is only available to the owner, this is a security gate — only the owner can invoke this tool.
217
+ - If the tool is missing from your tool list, suggest upgrading the plugin.
209
218
 
210
219
  ## Connect
211
220