@alfe.ai/openclaw-identity 0.0.10 → 0.0.12
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 +26 -1
- package/dist/index.d.cts +146 -1
- package/dist/index.d.ts +146 -1
- package/dist/index.js +21 -1
- package/dist/plugin2.cjs +192 -223
- package/dist/plugin2.js +192 -223
- package/package.json +2 -1
package/dist/plugin2.cjs
CHANGED
|
@@ -2,102 +2,123 @@ let _alfe_ai_config = require("@alfe.ai/config");
|
|
|
2
2
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
3
3
|
let _sinclair_typebox = require("@sinclair/typebox");
|
|
4
4
|
let node_module = require("node:module");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setLastResolved,
|
|
50
|
-
getLastResolved,
|
|
51
|
-
getPerms,
|
|
52
|
-
clear
|
|
5
|
+
let _auriclabs_roles = require("@auriclabs/roles");
|
|
6
|
+
//#region src/agent-exec-gate.ts
|
|
7
|
+
/**
|
|
8
|
+
* Phase 1 Stage E — `agent:exec` gate evaluation.
|
|
9
|
+
*
|
|
10
|
+
* Round 6 strict-namespacing gate context (`{tool, args}`): the gate
|
|
11
|
+
* passes the tool name as a sift CONTEXT field, not as the action.
|
|
12
|
+
* Permissions narrow which tools an identity may exec via sift
|
|
13
|
+
* conditions on the `tool` field. `args` is its own field — never
|
|
14
|
+
* spread — so a tool whose args legitimately include a `tool` key
|
|
15
|
+
* can't override the gate's value (closes the round-5 toolArgs.tool
|
|
16
|
+
* collision).
|
|
17
|
+
*
|
|
18
|
+
* The function is a thin wrapper around `createAbility(...).has(...)`
|
|
19
|
+
* that asserts the architecture-invariant "exactly one ability.has
|
|
20
|
+
* call per gate" rule from the verify-roles-architecture script. Any
|
|
21
|
+
* caller that loops over multiple scopes and OR-aggregates the result
|
|
22
|
+
* silently loses `cannot` semantics — the wrapper makes the contract
|
|
23
|
+
* explicit and testable.
|
|
24
|
+
*
|
|
25
|
+
* Today this function is wired into `before_tool_call` only for the
|
|
26
|
+
* TOKEN path (B.0.7 atomic flip carries the token's `Permission[]` in
|
|
27
|
+
* `ctx.tokenPermissions`). The JWT path still falls back to permissive
|
|
28
|
+
* because the agent runtime can't yet fetch the resolved permission
|
|
29
|
+
* set for `ctx.actingIdentityId` (needs an AgentApiClient endpoint
|
|
30
|
+
* that doesn't exist yet — landing alongside Stage D's full rollout).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Evaluate the round-6 strict-namespacing gate. Single
|
|
34
|
+
* `ability.has({...}, {tool, args})` call per invocation — never
|
|
35
|
+
* fan out across multiple permission shapes.
|
|
36
|
+
*
|
|
37
|
+
* `permissions` is whatever the caller already has on hand (a token's
|
|
38
|
+
* `Permission[]`, a session-cached resolved set, etc.). The function
|
|
39
|
+
* doesn't fetch, doesn't cache, doesn't memoize — those concerns live
|
|
40
|
+
* one layer up.
|
|
41
|
+
*/
|
|
42
|
+
function evaluateAgentExec(permissions, args) {
|
|
43
|
+
if (!args.agentId) return { allowed: false };
|
|
44
|
+
const ability = (0, _auriclabs_roles.createAbility)(permissions);
|
|
45
|
+
const gatePermission = {
|
|
46
|
+
subject: "agent",
|
|
47
|
+
action: "exec",
|
|
48
|
+
scope: `agent:${args.agentId}`
|
|
53
49
|
};
|
|
50
|
+
return { allowed: ability.has(gatePermission, {
|
|
51
|
+
tool: args.toolName,
|
|
52
|
+
args: args.toolArgs
|
|
53
|
+
}) };
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* Translate the gate decision into the before_tool_call hook's return
|
|
57
|
+
* shape. `undefined` means "no opinion — allow"; `{ block: true, ... }`
|
|
58
|
+
* means deny.
|
|
59
|
+
*
|
|
60
|
+
* - `allowed === true` → undefined (allow regardless of mode).
|
|
61
|
+
* - `allowed === false` + mode === "fail-closed" → block.
|
|
62
|
+
* - `allowed === false` + mode === "fail-open" → undefined (warn-only;
|
|
63
|
+
* caller should log the would-deny but let it through). Returning
|
|
64
|
+
* undefined here is what makes the rollout safe — Stage H flips the
|
|
65
|
+
* env to fail-closed once the deny-rate metric stabilizes.
|
|
58
66
|
*/
|
|
59
|
-
function
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
67
|
+
function applyGateMode(decision, mode, toolName) {
|
|
68
|
+
if (decision.allowed) return void 0;
|
|
69
|
+
if (mode === "fail-open") return void 0;
|
|
70
|
+
return {
|
|
79
71
|
block: true,
|
|
80
|
-
blockReason: `
|
|
72
|
+
blockReason: `Permissions: tool '${toolName}' not allowed for this identity`
|
|
81
73
|
};
|
|
82
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the kill-switch from process.env. Defaults to "fail-closed"
|
|
77
|
+
* in production and "fail-open" elsewhere — Stage H flips production
|
|
78
|
+
* to fail-closed once metrics confirm a stable deny rate.
|
|
79
|
+
*/
|
|
80
|
+
function resolveToolGatingMode() {
|
|
81
|
+
const raw = process.env.OPENCLAW_TOOL_GATING;
|
|
82
|
+
if (raw === "fail-open" || raw === "fail-closed") return raw;
|
|
83
|
+
return "fail-open";
|
|
84
|
+
}
|
|
83
85
|
//#endregion
|
|
84
86
|
//#region src/plugin.ts
|
|
85
87
|
/**
|
|
86
88
|
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
87
89
|
*
|
|
88
|
-
* HTTP-based identity resolution
|
|
89
|
-
*
|
|
90
|
+
* HTTP-based identity resolution + AccessConfig admission gate. Installed as
|
|
91
|
+
* part of the core alfe integration on every agent.
|
|
90
92
|
*
|
|
91
93
|
* Hooks:
|
|
92
|
-
* - message_received →
|
|
93
|
-
*
|
|
94
|
-
* -
|
|
94
|
+
* - message_received → always resolves the sender, gates on accessAllowed,
|
|
95
|
+
* blocks unknown senders (read-only resolve).
|
|
96
|
+
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
97
|
+
* hook (Phase 1 Stage A.0). Today: permissive no-op
|
|
98
|
+
* stub. Phase 1 Stage E swaps the stub for the
|
|
99
|
+
* `agent:exec` + sift gate (Decision 17).
|
|
95
100
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
101
|
+
* `after_tool_call` is intentionally absent — tool-call audit lives in a
|
|
102
|
+
* future dedicated audit service, not in identity.
|
|
98
103
|
*/
|
|
99
104
|
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
100
|
-
const
|
|
105
|
+
const RESOLVE_CACHE_TTL_MS = 6e4;
|
|
106
|
+
const resolveCache = /* @__PURE__ */ new Map();
|
|
107
|
+
function getCachedResolve(key) {
|
|
108
|
+
const entry = resolveCache.get(key);
|
|
109
|
+
if (!entry) return null;
|
|
110
|
+
if (Date.now() > entry.expiresAt) {
|
|
111
|
+
resolveCache.delete(key);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
function setCachedResolve(key, value) {
|
|
117
|
+
resolveCache.set(key, {
|
|
118
|
+
...value,
|
|
119
|
+
expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
|
|
120
|
+
});
|
|
121
|
+
}
|
|
101
122
|
function ok(data) {
|
|
102
123
|
return {
|
|
103
124
|
content: [{
|
|
@@ -134,7 +155,7 @@ function defineTool(def) {
|
|
|
134
155
|
const plugin = {
|
|
135
156
|
id: "@alfe.ai/openclaw-identity",
|
|
136
157
|
name: "Alfe Identity",
|
|
137
|
-
description: "Identity resolution
|
|
158
|
+
description: "Identity resolution and access gating for inbound messages",
|
|
138
159
|
version: pkg.version,
|
|
139
160
|
activate(api) {
|
|
140
161
|
const log = api.logger;
|
|
@@ -150,26 +171,17 @@ const plugin = {
|
|
|
150
171
|
log.error(`Identity plugin: failed to resolve config — ${err instanceof Error ? err.message : String(err)}`);
|
|
151
172
|
return;
|
|
152
173
|
}
|
|
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
174
|
const tools = [
|
|
163
175
|
defineTool({
|
|
164
176
|
name: "who_is_this",
|
|
165
|
-
description: "Look up
|
|
177
|
+
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.",
|
|
166
178
|
parameters: _sinclair_typebox.Type.Object({
|
|
167
|
-
|
|
168
|
-
platformId: _sinclair_typebox.Type.String({ description: "
|
|
179
|
+
provider: _sinclair_typebox.Type.String({ description: "Provider name (discord, slack, chat, google-chat, clerk, etc.)" }),
|
|
180
|
+
platformId: _sinclair_typebox.Type.String({ description: "Provider-specific user identifier" })
|
|
169
181
|
}),
|
|
170
182
|
handler: async (params) => {
|
|
171
183
|
const result = await client.resolveIdentity({
|
|
172
|
-
|
|
184
|
+
provider: params.provider,
|
|
173
185
|
platformId: params.platformId
|
|
174
186
|
});
|
|
175
187
|
if (!result.identityId) return { found: false };
|
|
@@ -178,37 +190,19 @@ const plugin = {
|
|
|
178
190
|
}),
|
|
179
191
|
defineTool({
|
|
180
192
|
name: "lookup_identity",
|
|
181
|
-
description: "Search identities by name
|
|
193
|
+
description: "Search identities by name. Returns multiple matches.",
|
|
182
194
|
parameters: _sinclair_typebox.Type.Object({
|
|
183
195
|
query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Text search query" })),
|
|
184
|
-
status: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
185
|
-
tag: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
186
|
-
platform: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
196
|
+
status: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
187
197
|
}),
|
|
188
198
|
handler: (params) => client.searchIdentities({
|
|
189
199
|
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: _sinclair_typebox.Type.Object({
|
|
199
|
-
platform: _sinclair_typebox.Type.String(),
|
|
200
|
-
platformId: _sinclair_typebox.Type.String(),
|
|
201
|
-
displayName: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
202
|
-
}),
|
|
203
|
-
handler: (params) => client.resolveIdentity({
|
|
204
|
-
platform: params.platform,
|
|
205
|
-
platformId: params.platformId,
|
|
206
|
-
displayName: params.displayName
|
|
200
|
+
status: params.status
|
|
207
201
|
})
|
|
208
202
|
}),
|
|
209
203
|
defineTool({
|
|
210
204
|
name: "merge_identities",
|
|
211
|
-
description: "Merge two identity records — transfers notes, tags, aliases
|
|
205
|
+
description: "Merge two identity records — transfers contacts, platforms, notes, tags, aliases to the survivor. Hard-move, transactional.",
|
|
212
206
|
parameters: _sinclair_typebox.Type.Object({
|
|
213
207
|
survivorId: _sinclair_typebox.Type.String({ description: "Identity to keep" }),
|
|
214
208
|
mergedId: _sinclair_typebox.Type.String({ description: "Identity to merge into survivor" })
|
|
@@ -223,29 +217,16 @@ const plugin = {
|
|
|
223
217
|
}),
|
|
224
218
|
defineTool({
|
|
225
219
|
name: "unmerge_identities",
|
|
226
|
-
description: "Reverse a merge — restore previously merged identity",
|
|
220
|
+
description: "Reverse a merge — restore previously merged identity.",
|
|
227
221
|
parameters: _sinclair_typebox.Type.Object({ mergedId: _sinclair_typebox.Type.String({ description: "Identity that was merged (has mergedInto pointer)" }) }),
|
|
228
222
|
handler: (params) => client.unmergeIdentity(params.mergedId, { changedBy: {
|
|
229
223
|
type: "agent",
|
|
230
224
|
id: "plugin"
|
|
231
225
|
} })
|
|
232
226
|
}),
|
|
233
|
-
defineTool({
|
|
234
|
-
name: "link_platform",
|
|
235
|
-
description: "Link a platform identity to an existing identity record",
|
|
236
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
237
|
-
identityId: _sinclair_typebox.Type.String(),
|
|
238
|
-
platform: _sinclair_typebox.Type.String(),
|
|
239
|
-
platformId: _sinclair_typebox.Type.String()
|
|
240
|
-
}),
|
|
241
|
-
handler: (params) => client.resolveIdentity({
|
|
242
|
-
platform: params.platform,
|
|
243
|
-
platformId: params.platformId
|
|
244
|
-
})
|
|
245
|
-
}),
|
|
246
227
|
defineTool({
|
|
247
228
|
name: "add_identity_note",
|
|
248
|
-
description: "Add an observation or note about
|
|
229
|
+
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.",
|
|
249
230
|
parameters: _sinclair_typebox.Type.Object({
|
|
250
231
|
identityId: _sinclair_typebox.Type.String(),
|
|
251
232
|
content: _sinclair_typebox.Type.String(),
|
|
@@ -262,7 +243,7 @@ const plugin = {
|
|
|
262
243
|
}),
|
|
263
244
|
defineTool({
|
|
264
245
|
name: "tag_identity",
|
|
265
|
-
description: "Add or remove a tag on an identity",
|
|
246
|
+
description: "Add or remove a tag on an identity.",
|
|
266
247
|
parameters: _sinclair_typebox.Type.Object({
|
|
267
248
|
identityId: _sinclair_typebox.Type.String(),
|
|
268
249
|
tag: _sinclair_typebox.Type.String(),
|
|
@@ -279,7 +260,7 @@ const plugin = {
|
|
|
279
260
|
}),
|
|
280
261
|
defineTool({
|
|
281
262
|
name: "get_identity_changelog",
|
|
282
|
-
description: "Get
|
|
263
|
+
description: "Get the changelog for an identity — versions, diffs, who changed what.",
|
|
283
264
|
parameters: _sinclair_typebox.Type.Object({
|
|
284
265
|
identityId: _sinclair_typebox.Type.String(),
|
|
285
266
|
limit: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number())
|
|
@@ -288,7 +269,7 @@ const plugin = {
|
|
|
288
269
|
}),
|
|
289
270
|
defineTool({
|
|
290
271
|
name: "rollback_identity",
|
|
291
|
-
description: "Revert an identity to a previous version",
|
|
272
|
+
description: "Revert an identity to a previous version.",
|
|
292
273
|
parameters: _sinclair_typebox.Type.Object({
|
|
293
274
|
identityId: _sinclair_typebox.Type.String(),
|
|
294
275
|
targetVersion: _sinclair_typebox.Type.Number()
|
|
@@ -302,79 +283,83 @@ const plugin = {
|
|
|
302
283
|
})
|
|
303
284
|
}),
|
|
304
285
|
defineTool({
|
|
305
|
-
name: "
|
|
306
|
-
description: "
|
|
307
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
308
|
-
platform: _sinclair_typebox.Type.String(),
|
|
309
|
-
senderId: _sinclair_typebox.Type.String(),
|
|
310
|
-
channelId: _sinclair_typebox.Type.Optional(_sinclair_typebox.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",
|
|
286
|
+
name: "update_identity",
|
|
287
|
+
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.",
|
|
321
288
|
parameters: _sinclair_typebox.Type.Object({
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
289
|
+
identityId: _sinclair_typebox.Type.String(),
|
|
290
|
+
name: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
291
|
+
avatarUrl: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
292
|
+
timezone: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String()),
|
|
293
|
+
locale: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String())
|
|
325
294
|
}),
|
|
326
|
-
handler: (params) =>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
})
|
|
295
|
+
handler: (params) => {
|
|
296
|
+
const { identityId, ...rest } = params;
|
|
297
|
+
return client.updateIdentity(identityId, rest);
|
|
298
|
+
}
|
|
331
299
|
}),
|
|
332
300
|
defineTool({
|
|
333
301
|
name: "request_identity_verification",
|
|
334
|
-
description: "Send a verification phrase to the
|
|
302
|
+
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`.",
|
|
335
303
|
parameters: _sinclair_typebox.Type.Object({
|
|
336
304
|
claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed" }),
|
|
337
305
|
requestingIdentityId: _sinclair_typebox.Type.String({ description: "Identity of the person making the claim" }),
|
|
338
|
-
|
|
339
|
-
requestingPlatformId: _sinclair_typebox.Type.String({ description: "Requester's
|
|
340
|
-
|
|
306
|
+
requestingProvider: _sinclair_typebox.Type.String({ description: "Provider the requester is on (discord, slack, chat, etc.)" }),
|
|
307
|
+
requestingPlatformId: _sinclair_typebox.Type.String({ description: "Requester's provider-specific user ID" }),
|
|
308
|
+
contactEmail: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
|
|
309
|
+
contactMobile: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
|
|
310
|
+
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'" }))
|
|
341
311
|
}),
|
|
342
|
-
handler: (params) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
312
|
+
handler: (params) => {
|
|
313
|
+
const contactEmail = params.contactEmail;
|
|
314
|
+
const contactMobile = params.contactMobile;
|
|
315
|
+
if (contactEmail && contactMobile) return Promise.resolve({ error: "Specify exactly one of contactEmail or contactMobile, not both" });
|
|
316
|
+
const contact = contactEmail ? {
|
|
317
|
+
channel: "email",
|
|
318
|
+
value: contactEmail
|
|
319
|
+
} : contactMobile ? {
|
|
320
|
+
channel: "mobile",
|
|
321
|
+
value: contactMobile
|
|
322
|
+
} : void 0;
|
|
323
|
+
return client.requestIdentityVerification({
|
|
324
|
+
claimedIdentityId: params.claimedIdentityId,
|
|
325
|
+
requestingIdentityId: params.requestingIdentityId,
|
|
326
|
+
requestingProvider: params.requestingProvider,
|
|
327
|
+
requestingPlatformId: params.requestingPlatformId,
|
|
328
|
+
preferredChannel: params.preferredChannel,
|
|
329
|
+
contact
|
|
330
|
+
});
|
|
331
|
+
}
|
|
349
332
|
}),
|
|
350
333
|
defineTool({
|
|
351
334
|
name: "confirm_identity_verification",
|
|
352
|
-
description: "Confirm a verification by
|
|
335
|
+
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).",
|
|
353
336
|
parameters: _sinclair_typebox.Type.Object({
|
|
337
|
+
claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed (matches request)" }),
|
|
354
338
|
verificationId: _sinclair_typebox.Type.String({ description: "Verification ID returned from request_identity_verification" }),
|
|
355
|
-
phrase: _sinclair_typebox.Type.String({ description: "Three-word phrase the person received via
|
|
339
|
+
phrase: _sinclair_typebox.Type.String({ description: "Three-word phrase the person received via mobile or email" })
|
|
356
340
|
}),
|
|
357
341
|
handler: (params) => client.confirmIdentityVerification({
|
|
342
|
+
claimedIdentityId: params.claimedIdentityId,
|
|
358
343
|
verificationId: params.verificationId,
|
|
359
344
|
phrase: params.phrase
|
|
360
345
|
})
|
|
361
346
|
})
|
|
362
347
|
];
|
|
363
|
-
for (const tool of tools)
|
|
364
|
-
api.registerTool(tool);
|
|
365
|
-
identityToolNames.add(tool.name);
|
|
366
|
-
}
|
|
348
|
+
for (const tool of tools) api.registerTool(tool);
|
|
367
349
|
log.info(`Registered ${String(tools.length)} identity tools`);
|
|
368
350
|
api.on("message_received", async (...args) => {
|
|
369
351
|
const event = args[0];
|
|
370
352
|
const ctx = args[1];
|
|
371
|
-
const
|
|
372
|
-
const preResolvedIdentityId = event.metadata?.IdentityId;
|
|
353
|
+
const provider = ctx.channelId ?? "unknown";
|
|
373
354
|
const senderId = event.metadata?.UserId ?? event.from;
|
|
374
355
|
if (!senderId) return;
|
|
375
|
-
const cacheKey = ctx.conversationId ?? `${
|
|
376
|
-
const cached =
|
|
356
|
+
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
357
|
+
const cached = getCachedResolve(cacheKey);
|
|
377
358
|
if (cached) {
|
|
359
|
+
if (cached.identityId == null) return {
|
|
360
|
+
block: true,
|
|
361
|
+
blockReason: "Identity: identity not provisioned for this channel"
|
|
362
|
+
};
|
|
378
363
|
if (!cached.accessAllowed) return {
|
|
379
364
|
block: true,
|
|
380
365
|
blockReason: "Identity: access denied for this sender"
|
|
@@ -382,63 +367,47 @@ const plugin = {
|
|
|
382
367
|
return;
|
|
383
368
|
}
|
|
384
369
|
try {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
identityId: preResolvedIdentityId,
|
|
388
|
-
accessAllowed: true,
|
|
389
|
-
status: "pre-resolved"
|
|
390
|
-
};
|
|
391
|
-
else resolveResult = await client.resolveIdentity({
|
|
392
|
-
platform,
|
|
370
|
+
const r = await client.resolveIdentity({
|
|
371
|
+
provider,
|
|
393
372
|
platformId: senderId
|
|
394
373
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
374
|
+
setCachedResolve(cacheKey, {
|
|
375
|
+
identityId: r.identityId,
|
|
376
|
+
accessAllowed: r.accessAllowed,
|
|
377
|
+
status: r.status
|
|
378
|
+
});
|
|
379
|
+
if (r.identityId == null) {
|
|
380
|
+
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
381
|
+
return {
|
|
382
|
+
block: true,
|
|
383
|
+
blockReason: "Identity: identity not provisioned for this channel"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (!r.accessAllowed) return {
|
|
405
387
|
block: true,
|
|
406
388
|
blockReason: "Identity: access denied for this sender"
|
|
407
389
|
};
|
|
408
|
-
log.info(`Identity resolved: ${senderId} → ${
|
|
390
|
+
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
409
391
|
} catch (e) {
|
|
410
392
|
log.error(`Identity resolution failed for ${senderId}: ${e.message}`);
|
|
411
|
-
|
|
393
|
+
return {
|
|
412
394
|
block: true,
|
|
413
|
-
blockReason: "Identity: identity service unavailable — access denied (closed
|
|
395
|
+
blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
|
|
414
396
|
};
|
|
415
397
|
}
|
|
416
398
|
}, { priority: 100 });
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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}`);
|
|
399
|
+
const beforeToolCallStub = (event, ctx) => {
|
|
400
|
+
if (ctx.authMethod === "token" && ctx.tokenPermissions !== void 0) {
|
|
401
|
+
const blocked = applyGateMode(evaluateAgentExec(ctx.tokenPermissions, {
|
|
402
|
+
agentId: ctx.agentId,
|
|
403
|
+
toolName: event.toolName,
|
|
404
|
+
toolArgs: event.toolArgs
|
|
405
|
+
}), resolveToolGatingMode(), event.toolName);
|
|
406
|
+
return Promise.resolve(blocked);
|
|
440
407
|
}
|
|
441
|
-
|
|
408
|
+
return Promise.resolve(void 0);
|
|
409
|
+
};
|
|
410
|
+
api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
|
|
442
411
|
log.info("Alfe Identity plugin activated");
|
|
443
412
|
}
|
|
444
413
|
};
|