@alfe.ai/openclaw-identity 0.0.11 → 0.0.13
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/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -325
- package/dist/plugin.js +1 -325
- package/dist/plugin2.cjs +420 -0
- package/dist/plugin2.js +415 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_plugin = require("./
|
|
5
|
+
const require_plugin = require("./plugin2.cjs");
|
|
6
6
|
//#region src/runtime-contract.ts
|
|
7
7
|
/**
|
|
8
8
|
* Build the gate context object used by sift evaluation. This is the
|
|
@@ -24,4 +24,4 @@ function buildGateContext(event) {
|
|
|
24
24
|
}
|
|
25
25
|
//#endregion
|
|
26
26
|
exports.buildGateContext = buildGateContext;
|
|
27
|
-
exports.default = require_plugin;
|
|
27
|
+
exports.default = require_plugin.plugin;
|
package/dist/index.js
CHANGED
package/dist/plugin.cjs
CHANGED
|
@@ -1,325 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
let _sinclair_typebox = require("@sinclair/typebox");
|
|
4
|
-
//#region src/plugin.ts
|
|
5
|
-
/**
|
|
6
|
-
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
7
|
-
*
|
|
8
|
-
* HTTP-based identity resolution + AccessConfig admission gate. Installed as
|
|
9
|
-
* part of the core alfe integration on every agent.
|
|
10
|
-
*
|
|
11
|
-
* Hooks:
|
|
12
|
-
* - message_received → always resolves the sender, gates on accessAllowed,
|
|
13
|
-
* blocks unknown senders (read-only resolve).
|
|
14
|
-
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
15
|
-
* hook (Phase 1 Stage A.0). Today: permissive no-op
|
|
16
|
-
* stub. Phase 1 Stage E swaps the stub for the
|
|
17
|
-
* `agent:exec` + sift gate (Decision 17).
|
|
18
|
-
*
|
|
19
|
-
* `after_tool_call` is intentionally absent — tool-call audit lives in a
|
|
20
|
-
* future dedicated audit service, not in identity.
|
|
21
|
-
*/
|
|
22
|
-
const pkg = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
23
|
-
const RESOLVE_CACHE_TTL_MS = 6e4;
|
|
24
|
-
const resolveCache = /* @__PURE__ */ new Map();
|
|
25
|
-
function getCachedResolve(key) {
|
|
26
|
-
const entry = resolveCache.get(key);
|
|
27
|
-
if (!entry) return null;
|
|
28
|
-
if (Date.now() > entry.expiresAt) {
|
|
29
|
-
resolveCache.delete(key);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
return entry;
|
|
33
|
-
}
|
|
34
|
-
function setCachedResolve(key, value) {
|
|
35
|
-
resolveCache.set(key, {
|
|
36
|
-
...value,
|
|
37
|
-
expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
function ok(data) {
|
|
41
|
-
return {
|
|
42
|
-
content: [{
|
|
43
|
-
type: "text",
|
|
44
|
-
text: JSON.stringify(data)
|
|
45
|
-
}],
|
|
46
|
-
details: data
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function errResult(message) {
|
|
50
|
-
return {
|
|
51
|
-
content: [{
|
|
52
|
-
type: "text",
|
|
53
|
-
text: JSON.stringify({ error: message })
|
|
54
|
-
}],
|
|
55
|
-
details: { error: message }
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function defineTool(def) {
|
|
59
|
-
return {
|
|
60
|
-
name: def.name,
|
|
61
|
-
description: def.description,
|
|
62
|
-
label: def.name,
|
|
63
|
-
parameters: def.parameters,
|
|
64
|
-
execute: async (_toolCallId, params) => {
|
|
65
|
-
try {
|
|
66
|
-
return ok(await def.handler(params));
|
|
67
|
-
} catch (e) {
|
|
68
|
-
return errResult(e.message);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const plugin = {
|
|
74
|
-
id: "@alfe.ai/openclaw-identity",
|
|
75
|
-
name: "Alfe Identity",
|
|
76
|
-
description: "Identity resolution and access gating for inbound messages",
|
|
77
|
-
version: pkg.version,
|
|
78
|
-
activate(api) {
|
|
79
|
-
const log = api.logger;
|
|
80
|
-
log.info("Alfe Identity plugin activating...");
|
|
81
|
-
let client;
|
|
82
|
-
try {
|
|
83
|
-
const config = (0, _alfe_ai_config.resolveConfig)();
|
|
84
|
-
client = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
85
|
-
apiKey: config.apiKey,
|
|
86
|
-
apiUrl: config.apiUrl
|
|
87
|
-
});
|
|
88
|
-
} catch (err) {
|
|
89
|
-
log.error(`Identity plugin: failed to resolve config — ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const tools = [
|
|
93
|
-
defineTool({
|
|
94
|
-
name: "who_is_this",
|
|
95
|
-
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.",
|
|
96
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
97
|
-
provider: _sinclair_typebox.Type.String({ description: "Provider name (discord, slack, chat, google-chat, clerk, etc.)" }),
|
|
98
|
-
platformId: _sinclair_typebox.Type.String({ description: "Provider-specific user identifier" })
|
|
99
|
-
}),
|
|
100
|
-
handler: async (params) => {
|
|
101
|
-
const result = await client.resolveIdentity({
|
|
102
|
-
provider: params.provider,
|
|
103
|
-
platformId: params.platformId
|
|
104
|
-
});
|
|
105
|
-
if (!result.identityId) return { found: false };
|
|
106
|
-
return client.getIdentityContext(result.identityId);
|
|
107
|
-
}
|
|
108
|
-
}),
|
|
109
|
-
defineTool({
|
|
110
|
-
name: "lookup_identity",
|
|
111
|
-
description: "Search identities by name. Returns multiple matches.",
|
|
112
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
113
|
-
query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Text search query" })),
|
|
114
|
-
status: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
115
|
-
}),
|
|
116
|
-
handler: (params) => client.searchIdentities({
|
|
117
|
-
q: params.query,
|
|
118
|
-
status: params.status
|
|
119
|
-
})
|
|
120
|
-
}),
|
|
121
|
-
defineTool({
|
|
122
|
-
name: "merge_identities",
|
|
123
|
-
description: "Merge two identity records — transfers contacts, platforms, notes, tags, aliases to the survivor. Hard-move, transactional.",
|
|
124
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
125
|
-
survivorId: _sinclair_typebox.Type.String({ description: "Identity to keep" }),
|
|
126
|
-
mergedId: _sinclair_typebox.Type.String({ description: "Identity to merge into survivor" })
|
|
127
|
-
}),
|
|
128
|
-
handler: (params) => client.mergeIdentities(params.survivorId, {
|
|
129
|
-
mergedId: params.mergedId,
|
|
130
|
-
changedBy: {
|
|
131
|
-
type: "agent",
|
|
132
|
-
id: "plugin"
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
}),
|
|
136
|
-
defineTool({
|
|
137
|
-
name: "unmerge_identities",
|
|
138
|
-
description: "Reverse a merge — restore previously merged identity.",
|
|
139
|
-
parameters: _sinclair_typebox.Type.Object({ mergedId: _sinclair_typebox.Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
|
|
140
|
-
handler: (params) => client.unmergeIdentity(params.mergedId, { changedBy: {
|
|
141
|
-
type: "agent",
|
|
142
|
-
id: "plugin"
|
|
143
|
-
} })
|
|
144
|
-
}),
|
|
145
|
-
defineTool({
|
|
146
|
-
name: "add_identity_note",
|
|
147
|
-
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.",
|
|
148
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
149
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
150
|
-
content: _sinclair_typebox.Type.String(),
|
|
151
|
-
category: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "observation, preference, relationship, context, or warning" }))
|
|
152
|
-
}),
|
|
153
|
-
handler: (params) => client.addIdentityNote(params.identityId, {
|
|
154
|
-
content: params.content,
|
|
155
|
-
category: params.category,
|
|
156
|
-
changedBy: {
|
|
157
|
-
type: "agent",
|
|
158
|
-
id: "plugin"
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
}),
|
|
162
|
-
defineTool({
|
|
163
|
-
name: "tag_identity",
|
|
164
|
-
description: "Add or remove a tag on an identity.",
|
|
165
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
166
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
167
|
-
tag: _sinclair_typebox.Type.String(),
|
|
168
|
-
action: _sinclair_typebox.Type.String({ description: "'add' or 'remove'" })
|
|
169
|
-
}),
|
|
170
|
-
handler: (params) => client.tagIdentity(params.identityId, {
|
|
171
|
-
tag: params.tag,
|
|
172
|
-
action: params.action,
|
|
173
|
-
changedBy: {
|
|
174
|
-
type: "agent",
|
|
175
|
-
id: "plugin"
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
}),
|
|
179
|
-
defineTool({
|
|
180
|
-
name: "get_identity_changelog",
|
|
181
|
-
description: "Get the changelog for an identity — versions, diffs, who changed what.",
|
|
182
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
183
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
184
|
-
limit: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number())
|
|
185
|
-
}),
|
|
186
|
-
handler: (params) => client.getIdentityChangelog(params.identityId, { limit: params.limit })
|
|
187
|
-
}),
|
|
188
|
-
defineTool({
|
|
189
|
-
name: "rollback_identity",
|
|
190
|
-
description: "Revert an identity to a previous version.",
|
|
191
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
192
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
193
|
-
targetVersion: _sinclair_typebox.Type.Number()
|
|
194
|
-
}),
|
|
195
|
-
handler: (params) => client.rollbackIdentity(params.identityId, {
|
|
196
|
-
targetVersion: params.targetVersion,
|
|
197
|
-
changedBy: {
|
|
198
|
-
type: "agent",
|
|
199
|
-
id: "plugin"
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
}),
|
|
203
|
-
defineTool({
|
|
204
|
-
name: "update_identity",
|
|
205
|
-
description: "Update display-shape fields on an identity: name / avatarUrl / timezone / locale. 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.",
|
|
206
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
207
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
208
|
-
name: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
209
|
-
avatarUrl: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
210
|
-
timezone: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
211
|
-
locale: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
212
|
-
}),
|
|
213
|
-
handler: (params) => {
|
|
214
|
-
const { identityId, ...rest } = params;
|
|
215
|
-
return client.updateIdentity(identityId, rest);
|
|
216
|
-
}
|
|
217
|
-
}),
|
|
218
|
-
defineTool({
|
|
219
|
-
name: "request_identity_verification",
|
|
220
|
-
description: "Send a verification phrase to verify the user controls an email or mobile endpoint. Use this when the user has just told you their email or mobile number and you want them to confirm it. Pass exactly one of `contactEmail` or `contactMobile`. The person must relay the phrase back via `confirm_identity_verification`.",
|
|
221
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
222
|
-
claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed" }),
|
|
223
|
-
requestingIdentityId: _sinclair_typebox.Type.String({ description: "Identity of the person making the claim" }),
|
|
224
|
-
requestingProvider: _sinclair_typebox.Type.String({ description: "Provider the requester is on (discord, slack, chat, etc.)" }),
|
|
225
|
-
requestingPlatformId: _sinclair_typebox.Type.String({ description: "Requester's provider-specific user ID" }),
|
|
226
|
-
contactEmail: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
|
|
227
|
-
contactMobile: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
|
|
228
|
-
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'" }))
|
|
229
|
-
}),
|
|
230
|
-
handler: (params) => {
|
|
231
|
-
const contactEmail = params.contactEmail;
|
|
232
|
-
const contactMobile = params.contactMobile;
|
|
233
|
-
if (contactEmail && contactMobile) return Promise.resolve({ error: "Specify exactly one of contactEmail or contactMobile, not both" });
|
|
234
|
-
const contact = contactEmail ? {
|
|
235
|
-
channel: "email",
|
|
236
|
-
value: contactEmail
|
|
237
|
-
} : contactMobile ? {
|
|
238
|
-
channel: "mobile",
|
|
239
|
-
value: contactMobile
|
|
240
|
-
} : void 0;
|
|
241
|
-
return client.requestIdentityVerification({
|
|
242
|
-
claimedIdentityId: params.claimedIdentityId,
|
|
243
|
-
requestingIdentityId: params.requestingIdentityId,
|
|
244
|
-
requestingProvider: params.requestingProvider,
|
|
245
|
-
requestingPlatformId: params.requestingPlatformId,
|
|
246
|
-
preferredChannel: params.preferredChannel,
|
|
247
|
-
contact
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}),
|
|
251
|
-
defineTool({
|
|
252
|
-
name: "confirm_identity_verification",
|
|
253
|
-
description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity).",
|
|
254
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
255
|
-
claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed (matches request)" }),
|
|
256
|
-
verificationId: _sinclair_typebox.Type.String({ description: "Verification ID returned from request_identity_verification" }),
|
|
257
|
-
phrase: _sinclair_typebox.Type.String({ description: "Three-word phrase the person received via mobile or email" })
|
|
258
|
-
}),
|
|
259
|
-
handler: (params) => client.confirmIdentityVerification({
|
|
260
|
-
claimedIdentityId: params.claimedIdentityId,
|
|
261
|
-
verificationId: params.verificationId,
|
|
262
|
-
phrase: params.phrase
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
];
|
|
266
|
-
for (const tool of tools) api.registerTool(tool);
|
|
267
|
-
log.info(`Registered ${String(tools.length)} identity tools`);
|
|
268
|
-
api.on("message_received", async (...args) => {
|
|
269
|
-
const event = args[0];
|
|
270
|
-
const ctx = args[1];
|
|
271
|
-
const provider = ctx.channelId ?? "unknown";
|
|
272
|
-
const senderId = event.metadata?.UserId ?? event.from;
|
|
273
|
-
if (!senderId) return;
|
|
274
|
-
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
275
|
-
const cached = getCachedResolve(cacheKey);
|
|
276
|
-
if (cached) {
|
|
277
|
-
if (cached.identityId == null) return {
|
|
278
|
-
block: true,
|
|
279
|
-
blockReason: "Identity: identity not provisioned for this channel"
|
|
280
|
-
};
|
|
281
|
-
if (!cached.accessAllowed) return {
|
|
282
|
-
block: true,
|
|
283
|
-
blockReason: "Identity: access denied for this sender"
|
|
284
|
-
};
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
const r = await client.resolveIdentity({
|
|
289
|
-
provider,
|
|
290
|
-
platformId: senderId
|
|
291
|
-
});
|
|
292
|
-
setCachedResolve(cacheKey, {
|
|
293
|
-
identityId: r.identityId,
|
|
294
|
-
accessAllowed: r.accessAllowed,
|
|
295
|
-
status: r.status
|
|
296
|
-
});
|
|
297
|
-
if (r.identityId == null) {
|
|
298
|
-
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
299
|
-
return {
|
|
300
|
-
block: true,
|
|
301
|
-
blockReason: "Identity: identity not provisioned for this channel"
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
if (!r.accessAllowed) return {
|
|
305
|
-
block: true,
|
|
306
|
-
blockReason: "Identity: access denied for this sender"
|
|
307
|
-
};
|
|
308
|
-
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
309
|
-
} catch (e) {
|
|
310
|
-
log.error(`Identity resolution failed for ${senderId}: ${e.message}`);
|
|
311
|
-
return {
|
|
312
|
-
block: true,
|
|
313
|
-
blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
}, { priority: 100 });
|
|
317
|
-
const beforeToolCallStub = (event, ctx) => {
|
|
318
|
-
return Promise.resolve(void 0);
|
|
319
|
-
};
|
|
320
|
-
api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
|
|
321
|
-
log.info("Alfe Identity plugin activated");
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
//#endregion
|
|
325
|
-
module.exports = plugin;
|
|
1
|
+
const require_plugin = require("./plugin2.cjs");
|
|
2
|
+
module.exports = require_plugin.plugin;
|
package/dist/plugin.js
CHANGED
|
@@ -1,326 +1,2 @@
|
|
|
1
|
-
import {
|
|
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/plugin.ts
|
|
6
|
-
/**
|
|
7
|
-
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
8
|
-
*
|
|
9
|
-
* HTTP-based identity resolution + AccessConfig admission gate. Installed as
|
|
10
|
-
* part of the core alfe integration on every agent.
|
|
11
|
-
*
|
|
12
|
-
* Hooks:
|
|
13
|
-
* - message_received → always resolves the sender, gates on accessAllowed,
|
|
14
|
-
* blocks unknown senders (read-only resolve).
|
|
15
|
-
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
16
|
-
* hook (Phase 1 Stage A.0). Today: permissive no-op
|
|
17
|
-
* stub. Phase 1 Stage E swaps the stub for the
|
|
18
|
-
* `agent:exec` + sift gate (Decision 17).
|
|
19
|
-
*
|
|
20
|
-
* `after_tool_call` is intentionally absent — tool-call audit lives in a
|
|
21
|
-
* future dedicated audit service, not in identity.
|
|
22
|
-
*/
|
|
23
|
-
const pkg = createRequire(import.meta.url)("../package.json");
|
|
24
|
-
const RESOLVE_CACHE_TTL_MS = 6e4;
|
|
25
|
-
const resolveCache = /* @__PURE__ */ new Map();
|
|
26
|
-
function getCachedResolve(key) {
|
|
27
|
-
const entry = resolveCache.get(key);
|
|
28
|
-
if (!entry) return null;
|
|
29
|
-
if (Date.now() > entry.expiresAt) {
|
|
30
|
-
resolveCache.delete(key);
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
function setCachedResolve(key, value) {
|
|
36
|
-
resolveCache.set(key, {
|
|
37
|
-
...value,
|
|
38
|
-
expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function ok(data) {
|
|
42
|
-
return {
|
|
43
|
-
content: [{
|
|
44
|
-
type: "text",
|
|
45
|
-
text: JSON.stringify(data)
|
|
46
|
-
}],
|
|
47
|
-
details: data
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function errResult(message) {
|
|
51
|
-
return {
|
|
52
|
-
content: [{
|
|
53
|
-
type: "text",
|
|
54
|
-
text: JSON.stringify({ error: message })
|
|
55
|
-
}],
|
|
56
|
-
details: { error: message }
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function defineTool(def) {
|
|
60
|
-
return {
|
|
61
|
-
name: def.name,
|
|
62
|
-
description: def.description,
|
|
63
|
-
label: def.name,
|
|
64
|
-
parameters: def.parameters,
|
|
65
|
-
execute: async (_toolCallId, params) => {
|
|
66
|
-
try {
|
|
67
|
-
return ok(await def.handler(params));
|
|
68
|
-
} catch (e) {
|
|
69
|
-
return errResult(e.message);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const plugin = {
|
|
75
|
-
id: "@alfe.ai/openclaw-identity",
|
|
76
|
-
name: "Alfe Identity",
|
|
77
|
-
description: "Identity resolution and access gating for inbound messages",
|
|
78
|
-
version: pkg.version,
|
|
79
|
-
activate(api) {
|
|
80
|
-
const log = api.logger;
|
|
81
|
-
log.info("Alfe Identity plugin activating...");
|
|
82
|
-
let client;
|
|
83
|
-
try {
|
|
84
|
-
const config = resolveConfig();
|
|
85
|
-
client = new AgentApiClient({
|
|
86
|
-
apiKey: config.apiKey,
|
|
87
|
-
apiUrl: config.apiUrl
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
log.error(`Identity plugin: failed to resolve config — ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const tools = [
|
|
94
|
-
defineTool({
|
|
95
|
-
name: "who_is_this",
|
|
96
|
-
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.",
|
|
97
|
-
parameters: Type.Object({
|
|
98
|
-
provider: Type.String({ description: "Provider name (discord, slack, chat, google-chat, clerk, etc.)" }),
|
|
99
|
-
platformId: Type.String({ description: "Provider-specific user identifier" })
|
|
100
|
-
}),
|
|
101
|
-
handler: async (params) => {
|
|
102
|
-
const result = await client.resolveIdentity({
|
|
103
|
-
provider: params.provider,
|
|
104
|
-
platformId: params.platformId
|
|
105
|
-
});
|
|
106
|
-
if (!result.identityId) return { found: false };
|
|
107
|
-
return client.getIdentityContext(result.identityId);
|
|
108
|
-
}
|
|
109
|
-
}),
|
|
110
|
-
defineTool({
|
|
111
|
-
name: "lookup_identity",
|
|
112
|
-
description: "Search identities by name. Returns multiple matches.",
|
|
113
|
-
parameters: Type.Object({
|
|
114
|
-
query: Type.Optional(Type.String({ description: "Text search query" })),
|
|
115
|
-
status: Type.Optional(Type.String())
|
|
116
|
-
}),
|
|
117
|
-
handler: (params) => client.searchIdentities({
|
|
118
|
-
q: params.query,
|
|
119
|
-
status: params.status
|
|
120
|
-
})
|
|
121
|
-
}),
|
|
122
|
-
defineTool({
|
|
123
|
-
name: "merge_identities",
|
|
124
|
-
description: "Merge two identity records — transfers contacts, platforms, notes, tags, aliases to the survivor. Hard-move, transactional.",
|
|
125
|
-
parameters: Type.Object({
|
|
126
|
-
survivorId: Type.String({ description: "Identity to keep" }),
|
|
127
|
-
mergedId: Type.String({ description: "Identity to merge into survivor" })
|
|
128
|
-
}),
|
|
129
|
-
handler: (params) => client.mergeIdentities(params.survivorId, {
|
|
130
|
-
mergedId: params.mergedId,
|
|
131
|
-
changedBy: {
|
|
132
|
-
type: "agent",
|
|
133
|
-
id: "plugin"
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
}),
|
|
137
|
-
defineTool({
|
|
138
|
-
name: "unmerge_identities",
|
|
139
|
-
description: "Reverse a merge — restore previously merged identity.",
|
|
140
|
-
parameters: Type.Object({ mergedId: Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
|
|
141
|
-
handler: (params) => client.unmergeIdentity(params.mergedId, { changedBy: {
|
|
142
|
-
type: "agent",
|
|
143
|
-
id: "plugin"
|
|
144
|
-
} })
|
|
145
|
-
}),
|
|
146
|
-
defineTool({
|
|
147
|
-
name: "add_identity_note",
|
|
148
|
-
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.",
|
|
149
|
-
parameters: Type.Object({
|
|
150
|
-
identityId: Type.String(),
|
|
151
|
-
content: Type.String(),
|
|
152
|
-
category: Type.Optional(Type.String({ description: "observation, preference, relationship, context, or warning" }))
|
|
153
|
-
}),
|
|
154
|
-
handler: (params) => client.addIdentityNote(params.identityId, {
|
|
155
|
-
content: params.content,
|
|
156
|
-
category: params.category,
|
|
157
|
-
changedBy: {
|
|
158
|
-
type: "agent",
|
|
159
|
-
id: "plugin"
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
}),
|
|
163
|
-
defineTool({
|
|
164
|
-
name: "tag_identity",
|
|
165
|
-
description: "Add or remove a tag on an identity.",
|
|
166
|
-
parameters: Type.Object({
|
|
167
|
-
identityId: Type.String(),
|
|
168
|
-
tag: Type.String(),
|
|
169
|
-
action: Type.String({ description: "'add' or 'remove'" })
|
|
170
|
-
}),
|
|
171
|
-
handler: (params) => client.tagIdentity(params.identityId, {
|
|
172
|
-
tag: params.tag,
|
|
173
|
-
action: params.action,
|
|
174
|
-
changedBy: {
|
|
175
|
-
type: "agent",
|
|
176
|
-
id: "plugin"
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
}),
|
|
180
|
-
defineTool({
|
|
181
|
-
name: "get_identity_changelog",
|
|
182
|
-
description: "Get the changelog for an identity — versions, diffs, who changed what.",
|
|
183
|
-
parameters: Type.Object({
|
|
184
|
-
identityId: Type.String(),
|
|
185
|
-
limit: Type.Optional(Type.Number())
|
|
186
|
-
}),
|
|
187
|
-
handler: (params) => client.getIdentityChangelog(params.identityId, { limit: params.limit })
|
|
188
|
-
}),
|
|
189
|
-
defineTool({
|
|
190
|
-
name: "rollback_identity",
|
|
191
|
-
description: "Revert an identity to a previous version.",
|
|
192
|
-
parameters: Type.Object({
|
|
193
|
-
identityId: Type.String(),
|
|
194
|
-
targetVersion: Type.Number()
|
|
195
|
-
}),
|
|
196
|
-
handler: (params) => client.rollbackIdentity(params.identityId, {
|
|
197
|
-
targetVersion: params.targetVersion,
|
|
198
|
-
changedBy: {
|
|
199
|
-
type: "agent",
|
|
200
|
-
id: "plugin"
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
}),
|
|
204
|
-
defineTool({
|
|
205
|
-
name: "update_identity",
|
|
206
|
-
description: "Update display-shape fields on an identity: name / avatarUrl / timezone / locale. 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.",
|
|
207
|
-
parameters: Type.Object({
|
|
208
|
-
identityId: Type.String(),
|
|
209
|
-
name: Type.Optional(Type.String()),
|
|
210
|
-
avatarUrl: Type.Optional(Type.String()),
|
|
211
|
-
timezone: Type.Optional(Type.String()),
|
|
212
|
-
locale: Type.Optional(Type.String())
|
|
213
|
-
}),
|
|
214
|
-
handler: (params) => {
|
|
215
|
-
const { identityId, ...rest } = params;
|
|
216
|
-
return client.updateIdentity(identityId, rest);
|
|
217
|
-
}
|
|
218
|
-
}),
|
|
219
|
-
defineTool({
|
|
220
|
-
name: "request_identity_verification",
|
|
221
|
-
description: "Send a verification phrase to verify the user controls an email or mobile endpoint. Use this when the user has just told you their email or mobile number and you want them to confirm it. Pass exactly one of `contactEmail` or `contactMobile`. The person must relay the phrase back via `confirm_identity_verification`.",
|
|
222
|
-
parameters: Type.Object({
|
|
223
|
-
claimedIdentityId: Type.String({ description: "Identity being claimed" }),
|
|
224
|
-
requestingIdentityId: Type.String({ description: "Identity of the person making the claim" }),
|
|
225
|
-
requestingProvider: Type.String({ description: "Provider the requester is on (discord, slack, chat, etc.)" }),
|
|
226
|
-
requestingPlatformId: Type.String({ description: "Requester's provider-specific user ID" }),
|
|
227
|
-
contactEmail: Type.Optional(Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
|
|
228
|
-
contactMobile: Type.Optional(Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
|
|
229
|
-
preferredChannel: Type.Optional(Type.String({ description: "When neither contactEmail nor contactMobile is provided, pick which existing verified contact to deliver to: 'mobile' or 'email'" }))
|
|
230
|
-
}),
|
|
231
|
-
handler: (params) => {
|
|
232
|
-
const contactEmail = params.contactEmail;
|
|
233
|
-
const contactMobile = params.contactMobile;
|
|
234
|
-
if (contactEmail && contactMobile) return Promise.resolve({ error: "Specify exactly one of contactEmail or contactMobile, not both" });
|
|
235
|
-
const contact = contactEmail ? {
|
|
236
|
-
channel: "email",
|
|
237
|
-
value: contactEmail
|
|
238
|
-
} : contactMobile ? {
|
|
239
|
-
channel: "mobile",
|
|
240
|
-
value: contactMobile
|
|
241
|
-
} : void 0;
|
|
242
|
-
return client.requestIdentityVerification({
|
|
243
|
-
claimedIdentityId: params.claimedIdentityId,
|
|
244
|
-
requestingIdentityId: params.requestingIdentityId,
|
|
245
|
-
requestingProvider: params.requestingProvider,
|
|
246
|
-
requestingPlatformId: params.requestingPlatformId,
|
|
247
|
-
preferredChannel: params.preferredChannel,
|
|
248
|
-
contact
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}),
|
|
252
|
-
defineTool({
|
|
253
|
-
name: "confirm_identity_verification",
|
|
254
|
-
description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity).",
|
|
255
|
-
parameters: Type.Object({
|
|
256
|
-
claimedIdentityId: Type.String({ description: "Identity being claimed (matches request)" }),
|
|
257
|
-
verificationId: Type.String({ description: "Verification ID returned from request_identity_verification" }),
|
|
258
|
-
phrase: Type.String({ description: "Three-word phrase the person received via mobile or email" })
|
|
259
|
-
}),
|
|
260
|
-
handler: (params) => client.confirmIdentityVerification({
|
|
261
|
-
claimedIdentityId: params.claimedIdentityId,
|
|
262
|
-
verificationId: params.verificationId,
|
|
263
|
-
phrase: params.phrase
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
];
|
|
267
|
-
for (const tool of tools) api.registerTool(tool);
|
|
268
|
-
log.info(`Registered ${String(tools.length)} identity tools`);
|
|
269
|
-
api.on("message_received", async (...args) => {
|
|
270
|
-
const event = args[0];
|
|
271
|
-
const ctx = args[1];
|
|
272
|
-
const provider = ctx.channelId ?? "unknown";
|
|
273
|
-
const senderId = event.metadata?.UserId ?? event.from;
|
|
274
|
-
if (!senderId) return;
|
|
275
|
-
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
276
|
-
const cached = getCachedResolve(cacheKey);
|
|
277
|
-
if (cached) {
|
|
278
|
-
if (cached.identityId == null) return {
|
|
279
|
-
block: true,
|
|
280
|
-
blockReason: "Identity: identity not provisioned for this channel"
|
|
281
|
-
};
|
|
282
|
-
if (!cached.accessAllowed) return {
|
|
283
|
-
block: true,
|
|
284
|
-
blockReason: "Identity: access denied for this sender"
|
|
285
|
-
};
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
try {
|
|
289
|
-
const r = await client.resolveIdentity({
|
|
290
|
-
provider,
|
|
291
|
-
platformId: senderId
|
|
292
|
-
});
|
|
293
|
-
setCachedResolve(cacheKey, {
|
|
294
|
-
identityId: r.identityId,
|
|
295
|
-
accessAllowed: r.accessAllowed,
|
|
296
|
-
status: r.status
|
|
297
|
-
});
|
|
298
|
-
if (r.identityId == null) {
|
|
299
|
-
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
300
|
-
return {
|
|
301
|
-
block: true,
|
|
302
|
-
blockReason: "Identity: identity not provisioned for this channel"
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
if (!r.accessAllowed) return {
|
|
306
|
-
block: true,
|
|
307
|
-
blockReason: "Identity: access denied for this sender"
|
|
308
|
-
};
|
|
309
|
-
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
310
|
-
} catch (e) {
|
|
311
|
-
log.error(`Identity resolution failed for ${senderId}: ${e.message}`);
|
|
312
|
-
return {
|
|
313
|
-
block: true,
|
|
314
|
-
blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
}, { priority: 100 });
|
|
318
|
-
const beforeToolCallStub = (event, ctx) => {
|
|
319
|
-
return Promise.resolve(void 0);
|
|
320
|
-
};
|
|
321
|
-
api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
|
|
322
|
-
log.info("Alfe Identity plugin activated");
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
//#endregion
|
|
1
|
+
import { t as plugin } from "./plugin2.js";
|
|
326
2
|
export { plugin as default };
|