@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/README.md +14 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +40 -28
- package/dist/index.d.ts +40 -28
- package/dist/index.js +2 -2
- package/dist/plugin.cjs +5 -1
- package/dist/plugin.d.cts +25 -15
- package/dist/plugin.d.ts +25 -15
- package/dist/plugin2.cjs +161 -193
- package/dist/plugin2.d.cts +2 -0
- package/dist/plugin2.d.ts +2 -0
- package/dist/plugin2.js +150 -182
- package/package.json +4 -3
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
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
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.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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()
|
|
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
|
|
163
|
-
if (entry.expiresAt
|
|
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
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 =
|
|
189
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
319
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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({
|
|
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({
|
|
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 {
|
|
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 =
|
|
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 (
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
552
|
-
if (isSelfScopedWhoIsThis(event.
|
|
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.
|
|
515
|
+
toolArgs: event.params
|
|
563
516
|
}), resolveToolGatingMode(), event.toolName);
|
|
564
|
-
const cacheKey =
|
|
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.
|
|
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.
|
|
529
|
+
toolArgs: event.params
|
|
577
530
|
}), resolveToolGatingMode(), event.toolName);
|
|
578
531
|
};
|
|
579
|
-
api.on("before_tool_call", (...args) =>
|
|
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
|
};
|