@alfe.ai/openclaw-identity 0.1.27 → 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 +39 -27
- package/dist/index.d.ts +39 -27
- package/dist/index.js +2 -2
- package/dist/plugin.cjs +5 -1
- package/dist/plugin.d.cts +2 -19
- package/dist/plugin.d.ts +2 -19
- package/dist/plugin2.cjs +149 -149
- package/dist/plugin2.js +150 -150
- package/package.json +4 -4
package/dist/plugin2.cjs
CHANGED
|
@@ -13,7 +13,7 @@ let _auriclabs_roles = require("@auriclabs/roles");
|
|
|
13
13
|
* Permissions narrow which tools an identity may exec via sift
|
|
14
14
|
* conditions on the `tool` field. `args` is its own field — never
|
|
15
15
|
* spread — so a tool whose args legitimately include a `tool` key
|
|
16
|
-
* 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
|
|
17
17
|
* collision).
|
|
18
18
|
*
|
|
19
19
|
* The function is a thin wrapper around `createAbility(...).has(...)`
|
|
@@ -23,12 +23,10 @@ let _auriclabs_roles = require("@auriclabs/roles");
|
|
|
23
23
|
* silently loses `cannot` semantics — the wrapper makes the contract
|
|
24
24
|
* explicit and testable.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
* set for `ctx.actingIdentityId` (needs an AgentApiClient endpoint
|
|
31
|
-
* 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.
|
|
32
30
|
*/
|
|
33
31
|
/**
|
|
34
32
|
* Evaluate the round-6 strict-namespacing gate. Single
|
|
@@ -74,51 +72,28 @@ function applyGateMode(decision, mode, toolName) {
|
|
|
74
72
|
};
|
|
75
73
|
}
|
|
76
74
|
/**
|
|
77
|
-
* Resolve the kill-switch from process.env.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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.
|
|
80
78
|
*/
|
|
81
79
|
function resolveToolGatingMode() {
|
|
82
80
|
const raw = process.env.OPENCLAW_TOOL_GATING;
|
|
83
81
|
if (raw === "fail-open" || raw === "fail-closed") return raw;
|
|
84
82
|
return "fail-open";
|
|
85
83
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Evaluate `agent:chat` — the inbound conversational gate (Layer 2 of the
|
|
88
|
-
* default-deny chat path). Mirrors `evaluateAgentExec` but with no
|
|
89
|
-
* tool/args context — chat admission only depends on whether the
|
|
90
|
-
* resolved identity holds `agent:chat` at the agent's scope.
|
|
91
|
-
*
|
|
92
|
-
* Layer 1 (`services/chat`) already gates this for chat WS + channel
|
|
93
|
-
* adapter inject. The OpenClaw plugin's `message_received` hook is
|
|
94
|
-
* defense-in-depth — it catches any path that reaches the agent
|
|
95
|
-
* runtime without going through Layer 1.
|
|
96
|
-
*/
|
|
97
|
-
function evaluateAgentChat(permissions, args) {
|
|
98
|
-
if (!args.agentId) return { allowed: false };
|
|
99
|
-
const ability = (0, _auriclabs_roles.createAbility)(permissions);
|
|
100
|
-
const gatePermission = {
|
|
101
|
-
subject: "agent",
|
|
102
|
-
action: "chat",
|
|
103
|
-
scope: `agent:${args.agentId}`
|
|
104
|
-
};
|
|
105
|
-
return { allowed: ability.has(gatePermission, {}) };
|
|
106
|
-
}
|
|
107
84
|
//#endregion
|
|
108
85
|
//#region src/plugin.ts
|
|
109
86
|
/**
|
|
110
87
|
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
111
88
|
*
|
|
112
|
-
* HTTP-based identity resolution + role-based
|
|
89
|
+
* HTTP-based identity resolution + role-based tool gate. Installed as
|
|
113
90
|
* part of the core alfe integration on every agent.
|
|
114
91
|
*
|
|
115
92
|
* Hooks:
|
|
116
|
-
* - message_received → resolves the sender via the agent API
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* and any identity without the permission. Layer 2
|
|
121
|
-
* 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.
|
|
122
97
|
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
123
98
|
* hook (Phase 1 Stage A.0). Gates `agent:exec`
|
|
124
99
|
* via `evaluateAgentExec` against the token's
|
|
@@ -132,18 +107,26 @@ function evaluateAgentChat(permissions, args) {
|
|
|
132
107
|
*/
|
|
133
108
|
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
134
109
|
const RESOLVE_CACHE_TTL_MS = 6e4;
|
|
110
|
+
const MAX_RESOLVE_CACHE_ENTRIES = 1e3;
|
|
135
111
|
const resolveCache = /* @__PURE__ */ new Map();
|
|
136
112
|
let lastInboundConversationId;
|
|
137
113
|
function getCachedResolve(key) {
|
|
138
114
|
const entry = resolveCache.get(key);
|
|
139
115
|
if (!entry) return null;
|
|
140
|
-
if (Date.now()
|
|
116
|
+
if (Date.now() >= entry.expiresAt) {
|
|
141
117
|
resolveCache.delete(key);
|
|
142
118
|
return null;
|
|
143
119
|
}
|
|
144
120
|
return entry;
|
|
145
121
|
}
|
|
146
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
|
+
}
|
|
147
130
|
resolveCache.set(key, {
|
|
148
131
|
...value,
|
|
149
132
|
expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
|
|
@@ -160,8 +143,11 @@ function findMostRelevantCacheEntry(probeKeys) {
|
|
|
160
143
|
}
|
|
161
144
|
let mostRecent = null;
|
|
162
145
|
const now = Date.now();
|
|
163
|
-
for (const entry of resolveCache
|
|
164
|
-
if (entry.expiresAt
|
|
146
|
+
for (const [key, entry] of resolveCache) {
|
|
147
|
+
if (entry.expiresAt <= now) {
|
|
148
|
+
resolveCache.delete(key);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
165
151
|
if (!mostRecent || entry.expiresAt > mostRecent.expiresAt) mostRecent = entry;
|
|
166
152
|
}
|
|
167
153
|
return mostRecent;
|
|
@@ -177,17 +163,14 @@ function isSelfScopedWhoIsThis(toolArgs, cacheKey) {
|
|
|
177
163
|
return argProvider === cached.senderProvider && argPlatformId === cached.senderPlatformId;
|
|
178
164
|
}
|
|
179
165
|
/**
|
|
180
|
-
* Resolve the inbound session identity
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* (`req_…`), not the real session key the cache is keyed on. Falls back to
|
|
185
|
-
* `params.conversationId` only when no inbound message has been observed
|
|
186
|
-
* 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.
|
|
187
170
|
*/
|
|
188
171
|
function resolveSessionIdentity(params) {
|
|
189
|
-
const conversationId =
|
|
190
|
-
if (
|
|
172
|
+
const conversationId = params.conversationId;
|
|
173
|
+
if (typeof conversationId !== "string" || conversationId.length === 0) return {
|
|
191
174
|
ok: false,
|
|
192
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."
|
|
193
176
|
};
|
|
@@ -230,29 +213,54 @@ function getClient() {
|
|
|
230
213
|
}
|
|
231
214
|
let cachedAgentContext = null;
|
|
232
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();
|
|
233
220
|
async function getAgentContext(client) {
|
|
234
221
|
if (cachedAgentContext) return cachedAgentContext;
|
|
235
222
|
if (inflightWhoami) return inflightWhoami;
|
|
236
|
-
|
|
223
|
+
if (Date.now() < whoamiRetryAfter) return null;
|
|
224
|
+
const generation = lifecycleGeneration;
|
|
225
|
+
const request = (async () => {
|
|
237
226
|
try {
|
|
238
227
|
const r = await client.whoami();
|
|
228
|
+
if (generation !== lifecycleGeneration) return null;
|
|
229
|
+
whoamiRetryAfter = 0;
|
|
239
230
|
cachedAgentContext = {
|
|
240
231
|
agentId: r.agentId,
|
|
241
232
|
tenantId: r.tenantId
|
|
242
233
|
};
|
|
243
234
|
return cachedAgentContext;
|
|
244
235
|
} catch {
|
|
236
|
+
if (generation === lifecycleGeneration) whoamiRetryAfter = Date.now() + WHOAMI_RETRY_COOLDOWN_MS;
|
|
245
237
|
return null;
|
|
246
|
-
} finally {
|
|
247
|
-
inflightWhoami = null;
|
|
248
238
|
}
|
|
249
239
|
})();
|
|
250
|
-
|
|
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;
|
|
251
259
|
}
|
|
252
260
|
const plugin = {
|
|
253
261
|
id: "@alfe.ai/openclaw-identity",
|
|
254
262
|
name: "Alfe Identity",
|
|
255
|
-
description: "Identity resolution and
|
|
263
|
+
description: "Identity resolution and role-based tool gating",
|
|
256
264
|
version: pkg.version,
|
|
257
265
|
activate(api) {
|
|
258
266
|
(0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-identity" });
|
|
@@ -283,8 +291,16 @@ const plugin = {
|
|
|
283
291
|
name: "lookup_identity",
|
|
284
292
|
description: "Search identities by name. Returns multiple matches.",
|
|
285
293
|
parameters: _sinclair_typebox.Type.Object({
|
|
286
|
-
query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
|
|
287
|
-
|
|
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
|
+
]))
|
|
288
304
|
}),
|
|
289
305
|
handler: (params) => client.searchIdentities({
|
|
290
306
|
q: params.query,
|
|
@@ -298,22 +314,13 @@ const plugin = {
|
|
|
298
314
|
survivorId: _sinclair_typebox.Type.String({ description: "Identity to keep" }),
|
|
299
315
|
mergedId: _sinclair_typebox.Type.String({ description: "Identity to merge into survivor" })
|
|
300
316
|
}),
|
|
301
|
-
handler: (params) => client.mergeIdentities(params.survivorId, {
|
|
302
|
-
mergedId: params.mergedId,
|
|
303
|
-
changedBy: {
|
|
304
|
-
type: "agent",
|
|
305
|
-
id: "plugin"
|
|
306
|
-
}
|
|
307
|
-
})
|
|
317
|
+
handler: (params) => client.mergeIdentities(params.survivorId, { mergedId: params.mergedId })
|
|
308
318
|
}),
|
|
309
319
|
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
310
320
|
name: "unmerge_identities",
|
|
311
321
|
description: "Reverse a merge — restore previously merged identity.",
|
|
312
322
|
parameters: _sinclair_typebox.Type.Object({ mergedId: _sinclair_typebox.Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
|
|
313
|
-
handler: (params) => client.unmergeIdentity(params.mergedId
|
|
314
|
-
type: "agent",
|
|
315
|
-
id: "plugin"
|
|
316
|
-
} })
|
|
323
|
+
handler: (params) => client.unmergeIdentity(params.mergedId)
|
|
317
324
|
}),
|
|
318
325
|
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
319
326
|
name: "add_identity_note",
|
|
@@ -321,15 +328,17 @@ const plugin = {
|
|
|
321
328
|
parameters: _sinclair_typebox.Type.Object({
|
|
322
329
|
identityId: _sinclair_typebox.Type.String(),
|
|
323
330
|
content: _sinclair_typebox.Type.String(),
|
|
324
|
-
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
|
+
]))
|
|
325
338
|
}),
|
|
326
339
|
handler: (params) => client.addIdentityNote(params.identityId, {
|
|
327
340
|
content: params.content,
|
|
328
|
-
category: params.category
|
|
329
|
-
changedBy: {
|
|
330
|
-
type: "agent",
|
|
331
|
-
id: "plugin"
|
|
332
|
-
}
|
|
341
|
+
category: params.category
|
|
333
342
|
})
|
|
334
343
|
}),
|
|
335
344
|
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
@@ -337,16 +346,15 @@ const plugin = {
|
|
|
337
346
|
description: "Add or remove a tag on an identity.",
|
|
338
347
|
parameters: _sinclair_typebox.Type.Object({
|
|
339
348
|
identityId: _sinclair_typebox.Type.String(),
|
|
340
|
-
tag: _sinclair_typebox.Type.String(
|
|
341
|
-
|
|
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")])
|
|
342
354
|
}),
|
|
343
355
|
handler: (params) => client.tagIdentity(params.identityId, {
|
|
344
356
|
tag: params.tag,
|
|
345
|
-
action: params.action
|
|
346
|
-
changedBy: {
|
|
347
|
-
type: "agent",
|
|
348
|
-
id: "plugin"
|
|
349
|
-
}
|
|
357
|
+
action: params.action
|
|
350
358
|
})
|
|
351
359
|
}),
|
|
352
360
|
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
@@ -354,7 +362,10 @@ const plugin = {
|
|
|
354
362
|
description: "Get the changelog for an identity — versions, diffs, who changed what.",
|
|
355
363
|
parameters: _sinclair_typebox.Type.Object({
|
|
356
364
|
identityId: _sinclair_typebox.Type.String(),
|
|
357
|
-
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
|
+
}))
|
|
358
369
|
}),
|
|
359
370
|
handler: (params) => client.getIdentityChangelog(params.identityId, { limit: params.limit })
|
|
360
371
|
}),
|
|
@@ -363,15 +374,9 @@ const plugin = {
|
|
|
363
374
|
description: "Revert an identity to a previous version.",
|
|
364
375
|
parameters: _sinclair_typebox.Type.Object({
|
|
365
376
|
identityId: _sinclair_typebox.Type.String(),
|
|
366
|
-
targetVersion: _sinclair_typebox.Type.
|
|
377
|
+
targetVersion: _sinclair_typebox.Type.Integer({ minimum: 0 })
|
|
367
378
|
}),
|
|
368
|
-
handler: (params) => client.rollbackIdentity(params.identityId, {
|
|
369
|
-
targetVersion: params.targetVersion,
|
|
370
|
-
changedBy: {
|
|
371
|
-
type: "agent",
|
|
372
|
-
id: "plugin"
|
|
373
|
-
}
|
|
374
|
-
})
|
|
379
|
+
handler: (params) => client.rollbackIdentity(params.identityId, { targetVersion: params.targetVersion })
|
|
375
380
|
}),
|
|
376
381
|
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
377
382
|
name: "update_identity",
|
|
@@ -385,7 +390,7 @@ const plugin = {
|
|
|
385
390
|
}),
|
|
386
391
|
handler: (params) => {
|
|
387
392
|
const { identityId, ...rest } = params;
|
|
388
|
-
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.`);
|
|
389
394
|
return client.updateIdentity(identityId, rest);
|
|
390
395
|
}
|
|
391
396
|
}),
|
|
@@ -396,12 +401,15 @@ const plugin = {
|
|
|
396
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." })),
|
|
397
402
|
contactEmail: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
|
|
398
403
|
contactMobile: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
|
|
399
|
-
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" }))
|
|
400
405
|
}),
|
|
401
406
|
handler: (params) => {
|
|
402
407
|
const contactEmail = params.contactEmail;
|
|
403
408
|
const contactMobile = params.contactMobile;
|
|
404
|
-
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
|
+
});
|
|
405
413
|
const contact = contactEmail ? {
|
|
406
414
|
channel: "email",
|
|
407
415
|
value: contactEmail
|
|
@@ -410,7 +418,10 @@ const plugin = {
|
|
|
410
418
|
value: contactMobile
|
|
411
419
|
} : void 0;
|
|
412
420
|
const resolved = resolveRequesterContext(params);
|
|
413
|
-
if ("error" in resolved) return Promise.resolve({
|
|
421
|
+
if ("error" in resolved) return Promise.resolve({
|
|
422
|
+
status: "error",
|
|
423
|
+
error: resolved.error
|
|
424
|
+
});
|
|
414
425
|
return client.requestIdentityVerification({
|
|
415
426
|
claimedIdentityId: resolved.claimedIdentityId,
|
|
416
427
|
requestingIdentityId: resolved.requestingIdentityId,
|
|
@@ -431,15 +442,18 @@ const plugin = {
|
|
|
431
442
|
}),
|
|
432
443
|
handler: async (params) => {
|
|
433
444
|
const resolved = resolveClaimedIdentity(params);
|
|
434
|
-
if ("error" in resolved) return {
|
|
445
|
+
if ("error" in resolved) return {
|
|
446
|
+
status: "error",
|
|
447
|
+
error: resolved.error
|
|
448
|
+
};
|
|
435
449
|
const result = await client.confirmIdentityVerification({
|
|
436
450
|
claimedIdentityId: resolved.claimedIdentityId,
|
|
437
451
|
verificationId: params.verificationId,
|
|
438
452
|
phrase: params.phrase
|
|
439
453
|
});
|
|
440
454
|
if (result.verified === true) {
|
|
441
|
-
const conversationId =
|
|
442
|
-
if (conversationId) invalidateCachedResolve(conversationId);
|
|
455
|
+
const conversationId = params.conversationId;
|
|
456
|
+
if (typeof conversationId === "string" && conversationId.length > 0) invalidateCachedResolve(conversationId);
|
|
443
457
|
}
|
|
444
458
|
return result;
|
|
445
459
|
}
|
|
@@ -450,34 +464,18 @@ const plugin = {
|
|
|
450
464
|
api.on("message_received", async (...args) => {
|
|
451
465
|
const event = args[0];
|
|
452
466
|
const ctx = args[1];
|
|
453
|
-
const provider = ctx.channelId ?? "unknown";
|
|
454
|
-
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;
|
|
455
469
|
if (!senderId) return;
|
|
456
470
|
if (ctx.conversationId) lastInboundConversationId = ctx.conversationId;
|
|
457
|
-
let agentId = ctx.agentId;
|
|
458
|
-
if (!agentId) {
|
|
459
|
-
const fallback = await getAgentContext(client);
|
|
460
|
-
if (fallback) agentId = fallback.agentId;
|
|
461
|
-
else {
|
|
462
|
-
log.warn("message_received without ctx.agentId AND whoami unavailable — fail-closed");
|
|
463
|
-
return {
|
|
464
|
-
block: true,
|
|
465
|
-
blockReason: "Identity: agentId missing on message context and whoami fallback unavailable"
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
471
|
const forwarded = Array.isArray(event.metadata?.SenderPermissions) ? event.metadata.SenderPermissions.filter((p) => typeof p === "string") : [];
|
|
470
472
|
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
471
473
|
const cached = getCachedResolve(cacheKey);
|
|
472
474
|
if (cached) {
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
};
|
|
477
|
-
if (!evaluateAgentChat([...cached.permissions, ...forwarded], { agentId }).allowed) return {
|
|
478
|
-
block: true,
|
|
479
|
-
blockReason: "Identity: chat not permitted for this sender"
|
|
480
|
-
};
|
|
475
|
+
if (forwarded.length > 0) setCachedResolve(cacheKey, {
|
|
476
|
+
...cached,
|
|
477
|
+
permissions: [...new Set([...cached.permissions, ...forwarded])]
|
|
478
|
+
});
|
|
481
479
|
return;
|
|
482
480
|
}
|
|
483
481
|
try {
|
|
@@ -492,32 +490,19 @@ const plugin = {
|
|
|
492
490
|
senderProvider: provider,
|
|
493
491
|
senderPlatformId: senderId
|
|
494
492
|
});
|
|
495
|
-
|
|
496
|
-
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
497
|
-
return {
|
|
498
|
-
block: true,
|
|
499
|
-
blockReason: "Identity: identity not provisioned for this channel"
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
const cachedAfterFill = getCachedResolve(cacheKey);
|
|
503
|
-
if (!evaluateAgentChat(cachedAfterFill ? [...cachedAfterFill.permissions] : [...r.permissions, ...forwarded], { agentId }).allowed) return {
|
|
504
|
-
block: true,
|
|
505
|
-
blockReason: "Identity: chat not permitted for this sender"
|
|
506
|
-
};
|
|
507
|
-
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
493
|
+
log.debug(`Identity context cached for ${provider} (${r.status})`);
|
|
508
494
|
} catch (e) {
|
|
509
|
-
log.error(`Identity resolution failed
|
|
510
|
-
return {
|
|
511
|
-
block: true,
|
|
512
|
-
blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
|
|
513
|
-
};
|
|
495
|
+
log.error(`Identity context resolution failed: ${e.message}`);
|
|
514
496
|
}
|
|
515
497
|
}, { priority: 100 });
|
|
516
|
-
const
|
|
517
|
-
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
|
+
} };
|
|
518
503
|
if (event.toolName === "who_is_this") {
|
|
519
|
-
const cacheKey =
|
|
520
|
-
if (isSelfScopedWhoIsThis(event.
|
|
504
|
+
const cacheKey = resolveToolConversation(ctx);
|
|
505
|
+
if (isSelfScopedWhoIsThis(event.params, cacheKey)) return;
|
|
521
506
|
}
|
|
522
507
|
let agentId = ctx.agentId;
|
|
523
508
|
if (!agentId) {
|
|
@@ -525,28 +510,32 @@ const plugin = {
|
|
|
525
510
|
if (fallback) agentId = fallback.agentId;
|
|
526
511
|
}
|
|
527
512
|
if (ctx.authMethod === "token" && ctx.tokenPermissions !== void 0) return applyGateMode(evaluateAgentExec(ctx.tokenPermissions, {
|
|
528
|
-
agentId,
|
|
513
|
+
agentId: agentId ?? "",
|
|
529
514
|
toolName: event.toolName,
|
|
530
|
-
toolArgs: event.
|
|
515
|
+
toolArgs: event.params
|
|
531
516
|
}), resolveToolGatingMode(), event.toolName);
|
|
532
|
-
const cacheKey =
|
|
517
|
+
const cacheKey = resolveToolConversation(ctx);
|
|
533
518
|
if (cacheKey) {
|
|
534
519
|
const cached = getCachedResolve(cacheKey);
|
|
535
520
|
if (cached && cached.permissions.length > 0) return applyGateMode(evaluateAgentExec([...cached.permissions], {
|
|
536
|
-
agentId,
|
|
521
|
+
agentId: agentId ?? "",
|
|
537
522
|
toolName: event.toolName,
|
|
538
|
-
toolArgs: event.
|
|
523
|
+
toolArgs: event.params
|
|
539
524
|
}), resolveToolGatingMode(), event.toolName);
|
|
540
525
|
}
|
|
541
526
|
return applyGateMode(evaluateAgentExec([], {
|
|
542
|
-
agentId,
|
|
527
|
+
agentId: agentId ?? "",
|
|
543
528
|
toolName: event.toolName,
|
|
544
|
-
toolArgs: event.
|
|
529
|
+
toolArgs: event.params
|
|
545
530
|
}), resolveToolGatingMode(), event.toolName);
|
|
546
531
|
};
|
|
547
|
-
api.on("before_tool_call", (...args) =>
|
|
532
|
+
api.on("before_tool_call", (...args) => beforeToolCall(args[0], args[1]), { priority: 100 });
|
|
548
533
|
api.on("before_agent_start", (...args) => {
|
|
549
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);
|
|
550
539
|
const cached = findMostRelevantCacheEntry([
|
|
551
540
|
ctx.conversationId,
|
|
552
541
|
ctx.sessionKey,
|
|
@@ -565,6 +554,12 @@ const plugin = {
|
|
|
565
554
|
"verification flow."
|
|
566
555
|
].join(" ") });
|
|
567
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 });
|
|
568
563
|
if (!cachedAgentContext) getAgentContext(client).then((c) => {
|
|
569
564
|
if (c) log.info(`Plugin context warmed: agent=${c.agentId} tenant=${c.tenantId}`);
|
|
570
565
|
else log.warn("Plugin context warm failed; will retry on next hook fire");
|
|
@@ -572,9 +567,14 @@ const plugin = {
|
|
|
572
567
|
log.info("Alfe Identity plugin activated");
|
|
573
568
|
},
|
|
574
569
|
deactivate(api) {
|
|
570
|
+
lifecycleGeneration += 1;
|
|
575
571
|
cachedClient = null;
|
|
576
572
|
cachedAgentContext = null;
|
|
577
573
|
inflightWhoami = null;
|
|
574
|
+
whoamiRetryAfter = 0;
|
|
575
|
+
resolveCache.clear();
|
|
576
|
+
conversationByRunId.clear();
|
|
577
|
+
lastInboundConversationId = void 0;
|
|
578
578
|
api.logger.info("Alfe Identity plugin deactivated");
|
|
579
579
|
}
|
|
580
580
|
};
|