@alfe.ai/openclaw-identity 0.0.8 → 0.0.10

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.
@@ -0,0 +1,446 @@
1
+ import { createRequire } from "node:module";
2
+ import { resolveConfig } from "@alfe.ai/config";
3
+ import { AgentApiClient } from "@alfe.ai/agent-api-client";
4
+ import { Type } from "@sinclair/typebox";
5
+ //#region src/policy-cache.ts
6
+ const CACHE_TTL_MS = 6e4;
7
+ function createPolicyCache() {
8
+ const sessionCache = /* @__PURE__ */ new Map();
9
+ let lastResolvedPolicy = null;
10
+ function getCached(key) {
11
+ const entry = sessionCache.get(key);
12
+ if (!entry) return null;
13
+ if (Date.now() > entry.expiresAt) {
14
+ sessionCache.delete(key);
15
+ return null;
16
+ }
17
+ return entry.value;
18
+ }
19
+ function setCached(key, value) {
20
+ sessionCache.set(key, {
21
+ value,
22
+ expiresAt: Date.now() + CACHE_TTL_MS
23
+ });
24
+ }
25
+ function setLastResolved(value) {
26
+ lastResolvedPolicy = {
27
+ value,
28
+ expiresAt: Date.now() + CACHE_TTL_MS
29
+ };
30
+ }
31
+ function getLastResolved() {
32
+ if (!lastResolvedPolicy) return null;
33
+ if (Date.now() > lastResolvedPolicy.expiresAt) {
34
+ lastResolvedPolicy = null;
35
+ return null;
36
+ }
37
+ return lastResolvedPolicy.value;
38
+ }
39
+ function getPerms(sessionKey) {
40
+ return getCached(sessionKey) ?? getLastResolved();
41
+ }
42
+ function clear() {
43
+ sessionCache.clear();
44
+ lastResolvedPolicy = null;
45
+ }
46
+ return {
47
+ getCached,
48
+ setCached,
49
+ setLastResolved,
50
+ getLastResolved,
51
+ getPerms,
52
+ clear
53
+ };
54
+ }
55
+ /**
56
+ * Evaluate whether a tool call should be blocked based on the cached policy
57
+ * and failure mode.
58
+ */
59
+ function evaluateToolAccess(perms, toolName, failureMode) {
60
+ if (!perms) {
61
+ if (failureMode === "permissive") return void 0;
62
+ return {
63
+ block: true,
64
+ blockReason: "Identity: no identity context established — tool access denied"
65
+ };
66
+ }
67
+ if (!perms.identified) {
68
+ if (failureMode === "permissive") return void 0;
69
+ return {
70
+ block: true,
71
+ blockReason: "Identity: unknown sender identity — tool access denied"
72
+ };
73
+ }
74
+ if (perms.deniedTools.includes("*") || perms.deniedTools.includes(toolName)) return {
75
+ block: true,
76
+ blockReason: `Identity: tool '${toolName}' is denied for your role`
77
+ };
78
+ if (perms.allowedTools.length > 0 && !perms.allowedTools.includes(toolName)) return {
79
+ block: true,
80
+ blockReason: `Identity: tool '${toolName}' is not in your allowed tools`
81
+ };
82
+ }
83
+ //#endregion
84
+ //#region src/plugin.ts
85
+ /**
86
+ * @alfe.ai/openclaw-identity — OpenClaw native plugin
87
+ *
88
+ * HTTP-based identity resolution, permission enforcement, and CRM tools.
89
+ * Installed as part of the core alfe integration on every agent.
90
+ *
91
+ * Hooks:
92
+ * - message_received → resolve sender identity via HTTP, cache, gate access
93
+ * - before_tool_call → enforce permissions from cached policy
94
+ * - after_tool_call → log tool execution audit
95
+ *
96
+ * All data access via AgentApiClient (/agent/identity/* routes).
97
+ * Uses the agent's own API key (from ~/.alfe/config.toml) for authentication.
98
+ */
99
+ const pkg = createRequire(import.meta.url)("../package.json");
100
+ const { getCached, setCached, setLastResolved, getPerms } = createPolicyCache();
101
+ function ok(data) {
102
+ return {
103
+ content: [{
104
+ type: "text",
105
+ text: JSON.stringify(data)
106
+ }],
107
+ details: data
108
+ };
109
+ }
110
+ function errResult(message) {
111
+ return {
112
+ content: [{
113
+ type: "text",
114
+ text: JSON.stringify({ error: message })
115
+ }],
116
+ details: { error: message }
117
+ };
118
+ }
119
+ function defineTool(def) {
120
+ return {
121
+ name: def.name,
122
+ description: def.description,
123
+ label: def.name,
124
+ parameters: def.parameters,
125
+ execute: async (_toolCallId, params) => {
126
+ try {
127
+ return ok(await def.handler(params));
128
+ } catch (e) {
129
+ return errResult(e.message);
130
+ }
131
+ }
132
+ };
133
+ }
134
+ const plugin = {
135
+ id: "@alfe.ai/openclaw-identity",
136
+ name: "Alfe Identity",
137
+ description: "Identity resolution, access gating, and permission enforcement",
138
+ version: pkg.version,
139
+ activate(api) {
140
+ const log = api.logger;
141
+ log.info("Alfe Identity plugin activating...");
142
+ let client;
143
+ try {
144
+ const config = resolveConfig();
145
+ client = new AgentApiClient({
146
+ apiKey: config.apiKey,
147
+ apiUrl: config.apiUrl
148
+ });
149
+ } catch (err) {
150
+ log.error(`Identity plugin: failed to resolve config — ${err instanceof Error ? err.message : String(err)}`);
151
+ return;
152
+ }
153
+ let failureMode = "open";
154
+ const configReady = client.getIntegrationConfig("alfe").then((alfeConfig) => {
155
+ const mode = alfeConfig.config.identity_failure_mode;
156
+ if (mode === "open" || mode === "closed" || mode === "permissive") failureMode = mode;
157
+ log.info(`Identity failure mode: ${failureMode}`);
158
+ }).catch(() => {
159
+ log.info(`Identity failure mode: ${failureMode} (default — config fetch failed)`);
160
+ });
161
+ const identityToolNames = /* @__PURE__ */ new Set();
162
+ const tools = [
163
+ defineTool({
164
+ name: "who_is_this",
165
+ description: "Look up full identity context by platform and ID — returns profile, notes, tags, platforms, and recent changelog",
166
+ parameters: Type.Object({
167
+ platform: Type.String({ description: "Platform name (discord, slack, chat, sms, whatsapp, etc.)" }),
168
+ platformId: Type.String({ description: "Platform-specific user identifier" })
169
+ }),
170
+ handler: async (params) => {
171
+ const result = await client.resolveIdentity({
172
+ platform: params.platform,
173
+ platformId: params.platformId
174
+ });
175
+ if (!result.identityId) return { found: false };
176
+ return client.getIdentityContext(result.identityId);
177
+ }
178
+ }),
179
+ defineTool({
180
+ name: "lookup_identity",
181
+ description: "Search identities by name, email, phone, tag, or platform. Returns multiple matches.",
182
+ parameters: Type.Object({
183
+ query: Type.Optional(Type.String({ description: "Text search query" })),
184
+ status: Type.Optional(Type.String()),
185
+ tag: Type.Optional(Type.String()),
186
+ platform: Type.Optional(Type.String())
187
+ }),
188
+ handler: (params) => client.searchIdentities({
189
+ q: params.query,
190
+ status: params.status,
191
+ tag: params.tag,
192
+ platform: params.platform
193
+ })
194
+ }),
195
+ defineTool({
196
+ name: "create_identity",
197
+ description: "Create a new identity record with profile fields",
198
+ parameters: Type.Object({
199
+ platform: Type.String(),
200
+ platformId: Type.String(),
201
+ displayName: Type.Optional(Type.String())
202
+ }),
203
+ handler: (params) => client.resolveIdentity({
204
+ platform: params.platform,
205
+ platformId: params.platformId,
206
+ displayName: params.displayName
207
+ })
208
+ }),
209
+ defineTool({
210
+ name: "merge_identities",
211
+ description: "Merge two identity records — transfers notes, tags, aliases, platforms to survivor",
212
+ parameters: Type.Object({
213
+ survivorId: Type.String({ description: "Identity to keep" }),
214
+ mergedId: Type.String({ description: "Identity to merge into survivor" })
215
+ }),
216
+ handler: (params) => client.mergeIdentities(params.survivorId, {
217
+ mergedId: params.mergedId,
218
+ changedBy: {
219
+ type: "agent",
220
+ id: "plugin"
221
+ }
222
+ })
223
+ }),
224
+ defineTool({
225
+ name: "unmerge_identities",
226
+ description: "Reverse a merge — restore previously merged identity",
227
+ parameters: Type.Object({ mergedId: Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
228
+ handler: (params) => client.unmergeIdentity(params.mergedId, { changedBy: {
229
+ type: "agent",
230
+ id: "plugin"
231
+ } })
232
+ }),
233
+ defineTool({
234
+ name: "link_platform",
235
+ description: "Link a platform identity to an existing identity record",
236
+ parameters: Type.Object({
237
+ identityId: Type.String(),
238
+ platform: Type.String(),
239
+ platformId: Type.String()
240
+ }),
241
+ handler: (params) => client.resolveIdentity({
242
+ platform: params.platform,
243
+ platformId: params.platformId
244
+ })
245
+ }),
246
+ defineTool({
247
+ name: "add_identity_note",
248
+ description: "Add an observation or note about a contact",
249
+ parameters: Type.Object({
250
+ identityId: Type.String(),
251
+ content: Type.String(),
252
+ category: Type.Optional(Type.String({ description: "observation, preference, relationship, context, or warning" }))
253
+ }),
254
+ handler: (params) => client.addIdentityNote(params.identityId, {
255
+ content: params.content,
256
+ category: params.category,
257
+ changedBy: {
258
+ type: "agent",
259
+ id: "plugin"
260
+ }
261
+ })
262
+ }),
263
+ defineTool({
264
+ name: "tag_identity",
265
+ description: "Add or remove a tag on an identity",
266
+ parameters: Type.Object({
267
+ identityId: Type.String(),
268
+ tag: Type.String(),
269
+ action: Type.String({ description: "'add' or 'remove'" })
270
+ }),
271
+ handler: (params) => client.tagIdentity(params.identityId, {
272
+ tag: params.tag,
273
+ action: params.action,
274
+ changedBy: {
275
+ type: "agent",
276
+ id: "plugin"
277
+ }
278
+ })
279
+ }),
280
+ defineTool({
281
+ name: "get_identity_changelog",
282
+ description: "Get full changelog for an identity — all versions, diffs, who changed what",
283
+ parameters: Type.Object({
284
+ identityId: Type.String(),
285
+ limit: Type.Optional(Type.Number())
286
+ }),
287
+ handler: (params) => client.getIdentityChangelog(params.identityId, { limit: params.limit })
288
+ }),
289
+ defineTool({
290
+ name: "rollback_identity",
291
+ description: "Revert an identity to a previous version",
292
+ parameters: Type.Object({
293
+ identityId: Type.String(),
294
+ targetVersion: Type.Number()
295
+ }),
296
+ handler: (params) => client.rollbackIdentity(params.identityId, {
297
+ targetVersion: params.targetVersion,
298
+ changedBy: {
299
+ type: "agent",
300
+ id: "plugin"
301
+ }
302
+ })
303
+ }),
304
+ defineTool({
305
+ name: "enforce_policy",
306
+ description: "Resolve sender identity and return their tool policy",
307
+ parameters: Type.Object({
308
+ platform: Type.String(),
309
+ senderId: Type.String(),
310
+ channelId: Type.Optional(Type.String())
311
+ }),
312
+ handler: (params) => client.enforcePolicy({
313
+ platform: params.platform,
314
+ senderId: params.senderId,
315
+ channelId: params.channelId
316
+ })
317
+ }),
318
+ defineTool({
319
+ name: "check_permission",
320
+ description: "Check if a specific tool call is allowed for a sender",
321
+ parameters: Type.Object({
322
+ platform: Type.String(),
323
+ senderId: Type.String(),
324
+ toolName: Type.String()
325
+ }),
326
+ handler: (params) => client.checkToolPermission({
327
+ platform: params.platform,
328
+ senderId: params.senderId,
329
+ toolName: params.toolName
330
+ })
331
+ }),
332
+ defineTool({
333
+ name: "request_identity_verification",
334
+ description: "Send a verification phrase to the claimed identity's phone (SMS) or email. The person must relay the phrase back to confirm they own that identity.",
335
+ parameters: Type.Object({
336
+ claimedIdentityId: Type.String({ description: "Identity being claimed" }),
337
+ requestingIdentityId: Type.String({ description: "Identity of the person making the claim" }),
338
+ requestingPlatform: Type.String({ description: "Platform the requester is on (discord, slack, chat, etc.)" }),
339
+ requestingPlatformId: Type.String({ description: "Requester's platform-specific user ID" }),
340
+ preferredChannel: Type.Optional(Type.String({ description: "'sms' or 'email'" }))
341
+ }),
342
+ handler: (params) => client.requestIdentityVerification({
343
+ claimedIdentityId: params.claimedIdentityId,
344
+ requestingIdentityId: params.requestingIdentityId,
345
+ requestingPlatform: params.requestingPlatform,
346
+ requestingPlatformId: params.requestingPlatformId,
347
+ preferredChannel: params.preferredChannel
348
+ })
349
+ }),
350
+ defineTool({
351
+ name: "confirm_identity_verification",
352
+ description: "Confirm a verification by providing the three-word phrase. On success, the requesting identity is merged into the claimed identity.",
353
+ parameters: Type.Object({
354
+ verificationId: Type.String({ description: "Verification ID returned from request_identity_verification" }),
355
+ phrase: Type.String({ description: "Three-word phrase the person received via SMS or email" })
356
+ }),
357
+ handler: (params) => client.confirmIdentityVerification({
358
+ verificationId: params.verificationId,
359
+ phrase: params.phrase
360
+ })
361
+ })
362
+ ];
363
+ for (const tool of tools) {
364
+ api.registerTool(tool);
365
+ identityToolNames.add(tool.name);
366
+ }
367
+ log.info(`Registered ${String(tools.length)} identity tools`);
368
+ api.on("message_received", async (...args) => {
369
+ const event = args[0];
370
+ const ctx = args[1];
371
+ const platform = ctx.channelId ?? "unknown";
372
+ const preResolvedIdentityId = event.metadata?.IdentityId;
373
+ const senderId = event.metadata?.UserId ?? event.from;
374
+ if (!senderId) return;
375
+ const cacheKey = ctx.conversationId ?? `${platform}:${senderId}`;
376
+ const cached = getCached(cacheKey);
377
+ if (cached) {
378
+ if (!cached.accessAllowed) return {
379
+ block: true,
380
+ blockReason: "Identity: access denied for this sender"
381
+ };
382
+ return;
383
+ }
384
+ try {
385
+ let resolveResult;
386
+ if (preResolvedIdentityId) resolveResult = {
387
+ identityId: preResolvedIdentityId,
388
+ accessAllowed: true,
389
+ status: "pre-resolved"
390
+ };
391
+ else resolveResult = await client.resolveIdentity({
392
+ platform,
393
+ platformId: senderId
394
+ });
395
+ const entry = {
396
+ ...await client.enforcePolicy({
397
+ platform,
398
+ senderId
399
+ }),
400
+ accessAllowed: resolveResult.accessAllowed
401
+ };
402
+ setCached(cacheKey, entry);
403
+ setLastResolved(entry);
404
+ if (!resolveResult.accessAllowed) return {
405
+ block: true,
406
+ blockReason: "Identity: access denied for this sender"
407
+ };
408
+ log.info(`Identity resolved: ${senderId} → ${resolveResult.identityId ?? "unknown"} (${resolveResult.status})`);
409
+ } catch (e) {
410
+ log.error(`Identity resolution failed for ${senderId}: ${e.message}`);
411
+ if (failureMode === "closed") return {
412
+ block: true,
413
+ blockReason: "Identity: identity service unavailable — access denied (closed mode)"
414
+ };
415
+ }
416
+ }, { priority: 100 });
417
+ api.on("before_tool_call", async (...args) => {
418
+ const event = args[0];
419
+ const ctx = args[1];
420
+ if (identityToolNames.has(event.toolName)) return;
421
+ const sessionKey = ctx.sessionKey;
422
+ if (!sessionKey) return;
423
+ await configReady;
424
+ return evaluateToolAccess(getPerms(sessionKey), event.toolName, failureMode);
425
+ }, { priority: 100 });
426
+ api.on("after_tool_call", async (...args) => {
427
+ const event = args[0];
428
+ const sessionKey = args[1].sessionKey;
429
+ if (!sessionKey) return;
430
+ const perms = getPerms(sessionKey);
431
+ if (!perms?.identityId) return;
432
+ try {
433
+ await client.checkToolPermission({
434
+ platform: "tool_audit",
435
+ senderId: perms.identityId,
436
+ toolName: event.toolName
437
+ });
438
+ } catch (e) {
439
+ log.error(`Audit logging failed: ${e.message}`);
440
+ }
441
+ });
442
+ log.info("Alfe Identity plugin activated");
443
+ }
444
+ };
445
+ //#endregion
446
+ export { plugin as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-identity",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "OpenClaw identity plugin — identity resolution, access gating, permission enforcement",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -28,13 +28,14 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@sinclair/typebox": "^0.34.48",
31
- "@alfe.ai/agent-api-client": "0.0.11",
31
+ "@alfe.ai/agent-api-client": "0.0.13",
32
32
  "@alfe.ai/config": "0.0.8"
33
33
  },
34
34
  "license": "UNLICENSED",
35
35
  "scripts": {
36
36
  "build": "tsdown",
37
37
  "dev": "tsdown --watch",
38
+ "test": "vitest run",
38
39
  "typecheck": "tsc --noEmit",
39
40
  "lint": "eslint ."
40
41
  }