@alfe.ai/openclaw-identity 0.1.26 → 0.1.28

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/plugin2.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  let _alfe_ai_config = require("@alfe.ai/config");
2
2
  let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
3
+ let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
3
4
  let _sinclair_typebox = require("@sinclair/typebox");
4
5
  let node_module = require("node:module");
5
6
  let _auriclabs_roles = require("@auriclabs/roles");
@@ -12,7 +13,7 @@ let _auriclabs_roles = require("@auriclabs/roles");
12
13
  * Permissions narrow which tools an identity may exec via sift
13
14
  * conditions on the `tool` field. `args` is its own field — never
14
15
  * spread — so a tool whose args legitimately include a `tool` key
15
- * can't override the gate's value (closes the round-5 toolArgs.tool
16
+ * can't override the gate's value (closes the round-5 parameter-name
16
17
  * collision).
17
18
  *
18
19
  * The function is a thin wrapper around `createAbility(...).has(...)`
@@ -22,12 +23,10 @@ let _auriclabs_roles = require("@auriclabs/roles");
22
23
  * silently loses `cannot` semantics — the wrapper makes the contract
23
24
  * explicit and testable.
24
25
  *
25
- * Today this function is wired into `before_tool_call` only for the
26
- * TOKEN path (B.0.7 atomic flip carries the token's `Permission[]` in
27
- * `ctx.tokenPermissions`). The JWT path still falls back to permissive
28
- * because the agent runtime can't yet fetch the resolved permission
29
- * set for `ctx.actingIdentityId` (needs an AgentApiClient endpoint
30
- * that doesn't exist yet — landing alongside Stage D's full rollout).
26
+ * The live `before_tool_call` hook uses this evaluator for both an optional
27
+ * Alfe-augmented token-permissions path and the resolved permission set cached
28
+ * from `message_received`. Stock OpenClaw does not expose auth-method or token
29
+ * permissions in its tool context, so the cache path is the production path.
31
30
  */
32
31
  /**
33
32
  * Evaluate the round-6 strict-namespacing gate. Single
@@ -73,51 +72,28 @@ function applyGateMode(decision, mode, toolName) {
73
72
  };
74
73
  }
75
74
  /**
76
- * Resolve the kill-switch from process.env. Defaults to "fail-closed"
77
- * in production and "fail-open" elsewhere Stage H flips production
78
- * to fail-closed once metrics confirm a stable deny rate.
75
+ * Resolve the kill-switch from process.env. It currently defaults to
76
+ * "fail-open" in every environment; rollout wiring must set "fail-closed"
77
+ * explicitly once metrics confirm a stable deny rate.
79
78
  */
80
79
  function resolveToolGatingMode() {
81
80
  const raw = process.env.OPENCLAW_TOOL_GATING;
82
81
  if (raw === "fail-open" || raw === "fail-closed") return raw;
83
82
  return "fail-open";
84
83
  }
85
- /**
86
- * Evaluate `agent:chat` — the inbound conversational gate (Layer 2 of the
87
- * default-deny chat path). Mirrors `evaluateAgentExec` but with no
88
- * tool/args context — chat admission only depends on whether the
89
- * resolved identity holds `agent:chat` at the agent's scope.
90
- *
91
- * Layer 1 (`services/chat`) already gates this for chat WS + channel
92
- * adapter inject. The OpenClaw plugin's `message_received` hook is
93
- * defense-in-depth — it catches any path that reaches the agent
94
- * runtime without going through Layer 1.
95
- */
96
- function evaluateAgentChat(permissions, args) {
97
- if (!args.agentId) return { allowed: false };
98
- const ability = (0, _auriclabs_roles.createAbility)(permissions);
99
- const gatePermission = {
100
- subject: "agent",
101
- action: "chat",
102
- scope: `agent:${args.agentId}`
103
- };
104
- return { allowed: ability.has(gatePermission, {}) };
105
- }
106
84
  //#endregion
107
85
  //#region src/plugin.ts
108
86
  /**
109
87
  * @alfe.ai/openclaw-identity — OpenClaw native plugin
110
88
  *
111
- * HTTP-based identity resolution + role-based admission gate. Installed as
89
+ * HTTP-based identity resolution + role-based tool gate. Installed as
112
90
  * part of the core alfe integration on every agent.
113
91
  *
114
92
  * Hooks:
115
- * - message_received → resolves the sender via the agent API and gates
116
- * on `agent:chat` at the agent's scope using the
117
- * resolved identity's permission set
118
- * (`evaluateAgentChat`). Blocks unknown senders
119
- * and any identity without the permission. Layer 2
120
- * of the default-deny chat path.
93
+ * - message_received → resolves and caches the sender via the agent API.
94
+ * OpenClaw runs this hook fire-and-forget and ignores
95
+ * return values, so it is context preparation, not an
96
+ * admission boundary.
121
97
  * - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
122
98
  * hook (Phase 1 Stage A.0). Gates `agent:exec`
123
99
  * via `evaluateAgentExec` against the token's
@@ -131,18 +107,26 @@ function evaluateAgentChat(permissions, args) {
131
107
  */
132
108
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
133
109
  const RESOLVE_CACHE_TTL_MS = 6e4;
110
+ const MAX_RESOLVE_CACHE_ENTRIES = 1e3;
134
111
  const resolveCache = /* @__PURE__ */ new Map();
135
112
  let lastInboundConversationId;
136
113
  function getCachedResolve(key) {
137
114
  const entry = resolveCache.get(key);
138
115
  if (!entry) return null;
139
- if (Date.now() > entry.expiresAt) {
116
+ if (Date.now() >= entry.expiresAt) {
140
117
  resolveCache.delete(key);
141
118
  return null;
142
119
  }
143
120
  return entry;
144
121
  }
145
122
  function setCachedResolve(key, value) {
123
+ const now = Date.now();
124
+ for (const [cachedKey, entry] of resolveCache) if (entry.expiresAt <= now) resolveCache.delete(cachedKey);
125
+ while (resolveCache.size >= MAX_RESOLVE_CACHE_ENTRIES) {
126
+ const oldest = resolveCache.keys().next();
127
+ if (oldest.done) break;
128
+ resolveCache.delete(oldest.value);
129
+ }
146
130
  resolveCache.set(key, {
147
131
  ...value,
148
132
  expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
@@ -159,8 +143,11 @@ function findMostRelevantCacheEntry(probeKeys) {
159
143
  }
160
144
  let mostRecent = null;
161
145
  const now = Date.now();
162
- for (const entry of resolveCache.values()) {
163
- if (entry.expiresAt < now) continue;
146
+ for (const [key, entry] of resolveCache) {
147
+ if (entry.expiresAt <= now) {
148
+ resolveCache.delete(key);
149
+ continue;
150
+ }
164
151
  if (!mostRecent || entry.expiresAt > mostRecent.expiresAt) mostRecent = entry;
165
152
  }
166
153
  return mostRecent;
@@ -176,17 +163,14 @@ function isSelfScopedWhoIsThis(toolArgs, cacheKey) {
176
163
  return argProvider === cached.senderProvider && argPlatformId === cached.senderPlatformId;
177
164
  }
178
165
  /**
179
- * Resolve the inbound session identity. Reads from `lastInboundConversationId`
180
- * (set by `message_received`) the LLM-supplied `params.conversationId` is
181
- * ignored because in practice the LLM copies the visible-metadata
182
- * `conversationId` field, which is currently the WS request envelope id
183
- * (`req_…`), not the real session key the cache is keyed on. Falls back to
184
- * `params.conversationId` only when no inbound message has been observed
185
- * this process (autonomous / scheduled triggers).
166
+ * Resolve the inbound session identity from the conversation value overwritten
167
+ * by `before_tool_call`. The hook replaces model-controlled input with the
168
+ * message-to-run bridge before execution. Autonomous and scheduled triggers
169
+ * receive an empty value and therefore have no fallback to a prior sender.
186
170
  */
187
171
  function resolveSessionIdentity(params) {
188
- const conversationId = lastInboundConversationId ?? params.conversationId;
189
- if (!conversationId) return {
172
+ const conversationId = params.conversationId;
173
+ if (typeof conversationId !== "string" || conversationId.length === 0) return {
190
174
  ok: false,
191
175
  error: "no_inbound_session: identity verification can only be initiated from a user-message turn. The verify tools auto-bind to the inbound sender's session and cannot be called from autonomous / scheduled / tool-chain triggers."
192
176
  };
@@ -217,39 +201,6 @@ function resolveClaimedIdentity(params) {
217
201
  if (!session.ok) return { error: session.error };
218
202
  return { claimedIdentityId: session.identityId };
219
203
  }
220
- function ok(data) {
221
- return {
222
- content: [{
223
- type: "text",
224
- text: JSON.stringify(data)
225
- }],
226
- details: data
227
- };
228
- }
229
- function errResult(message) {
230
- return {
231
- content: [{
232
- type: "text",
233
- text: JSON.stringify({ error: message })
234
- }],
235
- details: { error: message }
236
- };
237
- }
238
- function defineTool(def) {
239
- return {
240
- name: def.name,
241
- description: def.description,
242
- label: def.name,
243
- parameters: def.parameters,
244
- execute: async (_toolCallId, params) => {
245
- try {
246
- return ok(await def.handler(params));
247
- } catch (e) {
248
- return errResult(e.message);
249
- }
250
- }
251
- };
252
- }
253
204
  let cachedClient = null;
254
205
  function getClient() {
255
206
  if (cachedClient) return cachedClient;
@@ -262,29 +213,54 @@ function getClient() {
262
213
  }
263
214
  let cachedAgentContext = null;
264
215
  let inflightWhoami = null;
216
+ let lifecycleGeneration = 0;
217
+ let whoamiRetryAfter = 0;
218
+ const WHOAMI_RETRY_COOLDOWN_MS = 5e3;
219
+ const conversationByRunId = /* @__PURE__ */ new Map();
265
220
  async function getAgentContext(client) {
266
221
  if (cachedAgentContext) return cachedAgentContext;
267
222
  if (inflightWhoami) return inflightWhoami;
268
- inflightWhoami = (async () => {
223
+ if (Date.now() < whoamiRetryAfter) return null;
224
+ const generation = lifecycleGeneration;
225
+ const request = (async () => {
269
226
  try {
270
227
  const r = await client.whoami();
228
+ if (generation !== lifecycleGeneration) return null;
229
+ whoamiRetryAfter = 0;
271
230
  cachedAgentContext = {
272
231
  agentId: r.agentId,
273
232
  tenantId: r.tenantId
274
233
  };
275
234
  return cachedAgentContext;
276
235
  } catch {
236
+ if (generation === lifecycleGeneration) whoamiRetryAfter = Date.now() + WHOAMI_RETRY_COOLDOWN_MS;
277
237
  return null;
278
- } finally {
279
- inflightWhoami = null;
280
238
  }
281
239
  })();
282
- return inflightWhoami;
240
+ inflightWhoami = request;
241
+ request.finally(() => {
242
+ if (inflightWhoami === request) inflightWhoami = null;
243
+ });
244
+ return request;
245
+ }
246
+ function resolveToolConversation(ctx) {
247
+ if (ctx.runId) {
248
+ const runConversation = conversationByRunId.get(ctx.runId);
249
+ if (runConversation) return runConversation;
250
+ }
251
+ return lastInboundConversationId ?? ctx.conversationId ?? ctx.sessionKey ?? null;
252
+ }
253
+ function resolveInboundConversation(ctx) {
254
+ if (ctx.runId) {
255
+ const runConversation = conversationByRunId.get(ctx.runId);
256
+ if (runConversation) return runConversation;
257
+ }
258
+ return lastInboundConversationId ?? null;
283
259
  }
284
260
  const plugin = {
285
261
  id: "@alfe.ai/openclaw-identity",
286
262
  name: "Alfe Identity",
287
- description: "Identity resolution and access gating for inbound messages",
263
+ description: "Identity resolution and role-based tool gating",
288
264
  version: pkg.version,
289
265
  activate(api) {
290
266
  (0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-identity" });
@@ -295,7 +271,7 @@ const plugin = {
295
271
  return typeof v === "function" ? v.bind(c) : v;
296
272
  } });
297
273
  const tools = [
298
- defineTool({
274
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
299
275
  name: "who_is_this",
300
276
  description: "Look up an identity by provider + platformId — returns full identity context (profile, contacts, platforms, notes, tags, recent changelog). Returns `{found:false}` for unknown senders. This tool DOES NOT create new identities.",
301
277
  parameters: _sinclair_typebox.Type.Object({
@@ -311,101 +287,98 @@ const plugin = {
311
287
  return client.getIdentityContext(result.identityId);
312
288
  }
313
289
  }),
314
- defineTool({
290
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
315
291
  name: "lookup_identity",
316
292
  description: "Search identities by name. Returns multiple matches.",
317
293
  parameters: _sinclair_typebox.Type.Object({
318
- query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Text search query" })),
319
- status: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
294
+ query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
295
+ description: "Text search query",
296
+ maxLength: 256
297
+ })),
298
+ status: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([
299
+ _sinclair_typebox.Type.Literal("anonymous"),
300
+ _sinclair_typebox.Type.Literal("partial"),
301
+ _sinclair_typebox.Type.Literal("identified"),
302
+ _sinclair_typebox.Type.Literal("verified")
303
+ ]))
320
304
  }),
321
305
  handler: (params) => client.searchIdentities({
322
306
  q: params.query,
323
307
  status: params.status
324
308
  })
325
309
  }),
326
- defineTool({
310
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
327
311
  name: "merge_identities",
328
312
  description: "Merge two identity records — transfers contacts, platforms, notes, tags, aliases to the survivor. Hard-move, transactional.",
329
313
  parameters: _sinclair_typebox.Type.Object({
330
314
  survivorId: _sinclair_typebox.Type.String({ description: "Identity to keep" }),
331
315
  mergedId: _sinclair_typebox.Type.String({ description: "Identity to merge into survivor" })
332
316
  }),
333
- handler: (params) => client.mergeIdentities(params.survivorId, {
334
- mergedId: params.mergedId,
335
- changedBy: {
336
- type: "agent",
337
- id: "plugin"
338
- }
339
- })
317
+ handler: (params) => client.mergeIdentities(params.survivorId, { mergedId: params.mergedId })
340
318
  }),
341
- defineTool({
319
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
342
320
  name: "unmerge_identities",
343
321
  description: "Reverse a merge — restore previously merged identity.",
344
322
  parameters: _sinclair_typebox.Type.Object({ mergedId: _sinclair_typebox.Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
345
- handler: (params) => client.unmergeIdentity(params.mergedId, { changedBy: {
346
- type: "agent",
347
- id: "plugin"
348
- } })
323
+ handler: (params) => client.unmergeIdentity(params.mergedId)
349
324
  }),
350
- defineTool({
325
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
351
326
  name: "add_identity_note",
352
327
  description: "Add an observation or note about an identity. Use category 'context' for unverified affiliation claims (self-reported title/company) — those do NOT belong on the identity row directly.",
353
328
  parameters: _sinclair_typebox.Type.Object({
354
329
  identityId: _sinclair_typebox.Type.String(),
355
330
  content: _sinclair_typebox.Type.String(),
356
- category: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "observation, preference, relationship, context, or warning" }))
331
+ category: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([
332
+ _sinclair_typebox.Type.Literal("observation"),
333
+ _sinclair_typebox.Type.Literal("preference"),
334
+ _sinclair_typebox.Type.Literal("relationship"),
335
+ _sinclair_typebox.Type.Literal("context"),
336
+ _sinclair_typebox.Type.Literal("warning")
337
+ ]))
357
338
  }),
358
339
  handler: (params) => client.addIdentityNote(params.identityId, {
359
340
  content: params.content,
360
- category: params.category,
361
- changedBy: {
362
- type: "agent",
363
- id: "plugin"
364
- }
341
+ category: params.category
365
342
  })
366
343
  }),
367
- defineTool({
344
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
368
345
  name: "tag_identity",
369
346
  description: "Add or remove a tag on an identity.",
370
347
  parameters: _sinclair_typebox.Type.Object({
371
348
  identityId: _sinclair_typebox.Type.String(),
372
- tag: _sinclair_typebox.Type.String(),
373
- action: _sinclair_typebox.Type.String({ description: "'add' or 'remove'" })
349
+ tag: _sinclair_typebox.Type.String({
350
+ minLength: 1,
351
+ maxLength: 128
352
+ }),
353
+ action: _sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("add"), _sinclair_typebox.Type.Literal("remove")])
374
354
  }),
375
355
  handler: (params) => client.tagIdentity(params.identityId, {
376
356
  tag: params.tag,
377
- action: params.action,
378
- changedBy: {
379
- type: "agent",
380
- id: "plugin"
381
- }
357
+ action: params.action
382
358
  })
383
359
  }),
384
- defineTool({
360
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
385
361
  name: "get_identity_changelog",
386
362
  description: "Get the changelog for an identity — versions, diffs, who changed what.",
387
363
  parameters: _sinclair_typebox.Type.Object({
388
364
  identityId: _sinclair_typebox.Type.String(),
389
- limit: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number())
365
+ limit: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Integer({
366
+ minimum: 1,
367
+ maximum: 200
368
+ }))
390
369
  }),
391
370
  handler: (params) => client.getIdentityChangelog(params.identityId, { limit: params.limit })
392
371
  }),
393
- defineTool({
372
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
394
373
  name: "rollback_identity",
395
374
  description: "Revert an identity to a previous version.",
396
375
  parameters: _sinclair_typebox.Type.Object({
397
376
  identityId: _sinclair_typebox.Type.String(),
398
- targetVersion: _sinclair_typebox.Type.Number()
377
+ targetVersion: _sinclair_typebox.Type.Integer({ minimum: 0 })
399
378
  }),
400
- handler: (params) => client.rollbackIdentity(params.identityId, {
401
- targetVersion: params.targetVersion,
402
- changedBy: {
403
- type: "agent",
404
- id: "plugin"
405
- }
406
- })
379
+ handler: (params) => client.rollbackIdentity(params.identityId, { targetVersion: params.targetVersion })
407
380
  }),
408
- defineTool({
381
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
409
382
  name: "update_identity",
410
383
  description: "Update display-shape fields on a CONTACT/person you know (an identity): name / avatarUrl / timezone / locale. NOT for your own agent profile — to set YOUR OWN profile picture use `set_avatar` or `generate_avatar`, never this tool with identityId:'agent'. Contact mutations go through the verify flow — this tool will NOT accept email or mobile. Title and company live on org membership rows, not here.",
411
384
  parameters: _sinclair_typebox.Type.Object({
@@ -417,23 +390,26 @@ const plugin = {
417
390
  }),
418
391
  handler: (params) => {
419
392
  const { identityId, ...rest } = params;
420
- if (!identityId.startsWith("idn_")) throw new Error(`"${identityId}" is not a contact identity (those start with "idn_"). update_identity edits OTHER people you know — NOT your own agent profile. To set YOUR OWN profile picture, call generate_avatar (from a prompt) or set_avatar (with the image url you already generated). To change your own voice, call set_voice.`);
393
+ if (!identityId.startsWith("idn_")) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`"${identityId}" is not a contact identity (those start with "idn_"). update_identity edits OTHER people you know — NOT your own agent profile. To set YOUR OWN profile picture, call generate_avatar (from a prompt) or set_avatar (with the image url you already generated). To change your own voice, call set_voice.`);
421
394
  return client.updateIdentity(identityId, rest);
422
395
  }
423
396
  }),
424
- defineTool({
397
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
425
398
  name: "request_identity_verification",
426
399
  description: "Send a verification phrase to the inbound user's email or mobile to prove they own that contact endpoint. Use when the inbound user has just told you their email or mobile number and you want them to confirm it. The verification ALWAYS binds to the inbound sender's identity (the WhatsApp / SMS / Discord / Google Chat / chat-web user who just spoke) — identity is server-authoritative, auto-derived from the current inbound session. There is no way to verify a different identity via this tool. If the contact you supply already lives on another existing identity (e.g. the user's Clerk web account), the merge happens automatically on confirm. To add a new contact to an already-verified canonical identity (e.g. add a new email to a Clerk account), the user must be talking from a session that already resolves to that canonical identity (post-merge, or chat-web logged in as Clerk).",
427
400
  parameters: _sinclair_typebox.Type.Object({
428
401
  conversationId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Deprecated and ignored — the tool auto-binds to the current inbound session. Retained as optional for back-compat with older agent prompts." })),
429
402
  contactEmail: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
430
403
  contactMobile: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
431
- preferredChannel: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "When neither contactEmail nor contactMobile is provided, pick which existing verified contact to deliver to: 'mobile' or 'email'" }))
404
+ preferredChannel: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("mobile"), _sinclair_typebox.Type.Literal("email")], { description: "When neither contactEmail nor contactMobile is provided, pick which existing verified contact to deliver to" }))
432
405
  }),
433
406
  handler: (params) => {
434
407
  const contactEmail = params.contactEmail;
435
408
  const contactMobile = params.contactMobile;
436
- if (contactEmail && contactMobile) return Promise.resolve({ error: "Specify exactly one of contactEmail or contactMobile, not both" });
409
+ if (contactEmail && contactMobile) return Promise.resolve({
410
+ status: "error",
411
+ error: "Specify exactly one of contactEmail or contactMobile, not both"
412
+ });
437
413
  const contact = contactEmail ? {
438
414
  channel: "email",
439
415
  value: contactEmail
@@ -442,7 +418,10 @@ const plugin = {
442
418
  value: contactMobile
443
419
  } : void 0;
444
420
  const resolved = resolveRequesterContext(params);
445
- if ("error" in resolved) return Promise.resolve({ error: resolved.error });
421
+ if ("error" in resolved) return Promise.resolve({
422
+ status: "error",
423
+ error: resolved.error
424
+ });
446
425
  return client.requestIdentityVerification({
447
426
  claimedIdentityId: resolved.claimedIdentityId,
448
427
  requestingIdentityId: resolved.requestingIdentityId,
@@ -453,7 +432,7 @@ const plugin = {
453
432
  });
454
433
  }
455
434
  }),
456
- defineTool({
435
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
457
436
  name: "confirm_identity_verification",
458
437
  description: "Confirm a verification by submitting the phrase the inbound user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor — the inbound identity merged into it) or 'contact_verified' (the contact was attached to the inbound identity). Identity is server-authoritative, auto-derived from the current inbound session — no override available.",
459
438
  parameters: _sinclair_typebox.Type.Object({
@@ -463,15 +442,18 @@ const plugin = {
463
442
  }),
464
443
  handler: async (params) => {
465
444
  const resolved = resolveClaimedIdentity(params);
466
- if ("error" in resolved) return { error: resolved.error };
445
+ if ("error" in resolved) return {
446
+ status: "error",
447
+ error: resolved.error
448
+ };
467
449
  const result = await client.confirmIdentityVerification({
468
450
  claimedIdentityId: resolved.claimedIdentityId,
469
451
  verificationId: params.verificationId,
470
452
  phrase: params.phrase
471
453
  });
472
454
  if (result.verified === true) {
473
- const conversationId = lastInboundConversationId ?? params.conversationId;
474
- if (conversationId) invalidateCachedResolve(conversationId);
455
+ const conversationId = params.conversationId;
456
+ if (typeof conversationId === "string" && conversationId.length > 0) invalidateCachedResolve(conversationId);
475
457
  }
476
458
  return result;
477
459
  }
@@ -482,34 +464,18 @@ const plugin = {
482
464
  api.on("message_received", async (...args) => {
483
465
  const event = args[0];
484
466
  const ctx = args[1];
485
- const provider = ctx.channelId ?? "unknown";
486
- const senderId = event.metadata?.UserId ?? event.from;
467
+ const provider = event.metadata?.provider ?? ctx.channelId ?? "unknown";
468
+ const senderId = event.metadata?.senderId ?? event.metadata?.UserId ?? event.from;
487
469
  if (!senderId) return;
488
470
  if (ctx.conversationId) lastInboundConversationId = ctx.conversationId;
489
- let agentId = ctx.agentId;
490
- if (!agentId) {
491
- const fallback = await getAgentContext(client);
492
- if (fallback) agentId = fallback.agentId;
493
- else {
494
- log.warn("message_received without ctx.agentId AND whoami unavailable — fail-closed");
495
- return {
496
- block: true,
497
- blockReason: "Identity: agentId missing on message context and whoami fallback unavailable"
498
- };
499
- }
500
- }
501
471
  const forwarded = Array.isArray(event.metadata?.SenderPermissions) ? event.metadata.SenderPermissions.filter((p) => typeof p === "string") : [];
502
472
  const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
503
473
  const cached = getCachedResolve(cacheKey);
504
474
  if (cached) {
505
- if (cached.identityId == null) return {
506
- block: true,
507
- blockReason: "Identity: identity not provisioned for this channel"
508
- };
509
- if (!evaluateAgentChat([...cached.permissions, ...forwarded], { agentId }).allowed) return {
510
- block: true,
511
- blockReason: "Identity: chat not permitted for this sender"
512
- };
475
+ if (forwarded.length > 0) setCachedResolve(cacheKey, {
476
+ ...cached,
477
+ permissions: [...new Set([...cached.permissions, ...forwarded])]
478
+ });
513
479
  return;
514
480
  }
515
481
  try {
@@ -524,32 +490,19 @@ const plugin = {
524
490
  senderProvider: provider,
525
491
  senderPlatformId: senderId
526
492
  });
527
- if (r.identityId == null) {
528
- log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
529
- return {
530
- block: true,
531
- blockReason: "Identity: identity not provisioned for this channel"
532
- };
533
- }
534
- const cachedAfterFill = getCachedResolve(cacheKey);
535
- if (!evaluateAgentChat(cachedAfterFill ? [...cachedAfterFill.permissions] : [...r.permissions, ...forwarded], { agentId }).allowed) return {
536
- block: true,
537
- blockReason: "Identity: chat not permitted for this sender"
538
- };
539
- log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
493
+ log.debug(`Identity context cached for ${provider} (${r.status})`);
540
494
  } catch (e) {
541
- log.error(`Identity resolution failed for ${senderId}: ${e.message}`);
542
- return {
543
- block: true,
544
- blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
545
- };
495
+ log.error(`Identity context resolution failed: ${e.message}`);
546
496
  }
547
497
  }, { priority: 100 });
548
- const beforeToolCallStub = async (event, ctx) => {
549
- if (UNCONDITIONAL_SELF_SERVICE_TOOLS.has(event.toolName)) return;
498
+ const beforeToolCall = async (event, ctx) => {
499
+ if (UNCONDITIONAL_SELF_SERVICE_TOOLS.has(event.toolName)) return { params: {
500
+ ...event.params,
501
+ conversationId: resolveInboundConversation(ctx) ?? ""
502
+ } };
550
503
  if (event.toolName === "who_is_this") {
551
- const cacheKey = ctx.conversationId ?? (ctx.channelId && ctx.actingIdentityId ? `${ctx.channelId}:${ctx.actingIdentityId}` : null);
552
- if (isSelfScopedWhoIsThis(event.toolArgs, cacheKey)) return;
504
+ const cacheKey = resolveToolConversation(ctx);
505
+ if (isSelfScopedWhoIsThis(event.params, cacheKey)) return;
553
506
  }
554
507
  let agentId = ctx.agentId;
555
508
  if (!agentId) {
@@ -557,28 +510,32 @@ const plugin = {
557
510
  if (fallback) agentId = fallback.agentId;
558
511
  }
559
512
  if (ctx.authMethod === "token" && ctx.tokenPermissions !== void 0) return applyGateMode(evaluateAgentExec(ctx.tokenPermissions, {
560
- agentId,
513
+ agentId: agentId ?? "",
561
514
  toolName: event.toolName,
562
- toolArgs: event.toolArgs
515
+ toolArgs: event.params
563
516
  }), resolveToolGatingMode(), event.toolName);
564
- const cacheKey = ctx.conversationId ?? (ctx.channelId && ctx.actingIdentityId ? `${ctx.channelId}:${ctx.actingIdentityId}` : null);
517
+ const cacheKey = resolveToolConversation(ctx);
565
518
  if (cacheKey) {
566
519
  const cached = getCachedResolve(cacheKey);
567
520
  if (cached && cached.permissions.length > 0) return applyGateMode(evaluateAgentExec([...cached.permissions], {
568
- agentId,
521
+ agentId: agentId ?? "",
569
522
  toolName: event.toolName,
570
- toolArgs: event.toolArgs
523
+ toolArgs: event.params
571
524
  }), resolveToolGatingMode(), event.toolName);
572
525
  }
573
526
  return applyGateMode(evaluateAgentExec([], {
574
- agentId,
527
+ agentId: agentId ?? "",
575
528
  toolName: event.toolName,
576
- toolArgs: event.toolArgs
529
+ toolArgs: event.params
577
530
  }), resolveToolGatingMode(), event.toolName);
578
531
  };
579
- api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
532
+ api.on("before_tool_call", (...args) => beforeToolCall(args[0], args[1]), { priority: 100 });
580
533
  api.on("before_agent_start", (...args) => {
581
534
  const ctx = args[1];
535
+ if (ctx.trigger && ctx.trigger !== "user") {
536
+ lastInboundConversationId = void 0;
537
+ return Promise.resolve(void 0);
538
+ } else if (ctx.runId && lastInboundConversationId) conversationByRunId.set(ctx.runId, lastInboundConversationId);
582
539
  const cached = findMostRelevantCacheEntry([
583
540
  ctx.conversationId,
584
541
  ctx.sessionKey,
@@ -597,6 +554,12 @@ const plugin = {
597
554
  "verification flow."
598
555
  ].join(" ") });
599
556
  }, { priority: 100 });
557
+ api.on("agent_end", (...args) => {
558
+ const ctx = args[1];
559
+ if (ctx.runId) conversationByRunId.delete(ctx.runId);
560
+ lastInboundConversationId = void 0;
561
+ return Promise.resolve();
562
+ }, { priority: 100 });
600
563
  if (!cachedAgentContext) getAgentContext(client).then((c) => {
601
564
  if (c) log.info(`Plugin context warmed: agent=${c.agentId} tenant=${c.tenantId}`);
602
565
  else log.warn("Plugin context warm failed; will retry on next hook fire");
@@ -604,9 +567,14 @@ const plugin = {
604
567
  log.info("Alfe Identity plugin activated");
605
568
  },
606
569
  deactivate(api) {
570
+ lifecycleGeneration += 1;
607
571
  cachedClient = null;
608
572
  cachedAgentContext = null;
609
573
  inflightWhoami = null;
574
+ whoamiRetryAfter = 0;
575
+ resolveCache.clear();
576
+ conversationByRunId.clear();
577
+ lastInboundConversationId = void 0;
610
578
  api.logger.info("Alfe Identity plugin deactivated");
611
579
  }
612
580
  };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.cjs";
2
+ export { plugin as default };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.js";
2
+ export { plugin as default };