@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/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
- //#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
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
- * Evaluate whether a tool call should be blocked based on the cached policy
57
- * and failure mode.
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 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 {
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: `Identity: tool '${toolName}' is not in your allowed tools`
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, permission enforcement, and CRM tools.
89
- * Installed as part of the core alfe integration on every agent.
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 → 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
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
- * All data access via AgentApiClient (/agent/identity/* routes).
97
- * Uses the agent's own API key (from ~/.alfe/config.toml) for authentication.
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 { getCached, setCached, setLastResolved, getPerms } = createPolicyCache();
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, access gating, and permission enforcement",
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 full identity context by platform and ID — returns profile, notes, tags, platforms, and recent changelog",
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
- platform: _sinclair_typebox.Type.String({ description: "Platform name (discord, slack, chat, sms, whatsapp, etc.)" }),
168
- platformId: _sinclair_typebox.Type.String({ description: "Platform-specific user identifier" })
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
- platform: params.platform,
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, email, phone, tag, or platform. Returns multiple matches.",
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, platforms to survivor",
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 a contact",
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 full changelog for an identity — all versions, diffs, who changed what",
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: "enforce_policy",
306
- description: "Resolve sender identity and return their tool policy",
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
- platform: _sinclair_typebox.Type.String(),
323
- senderId: _sinclair_typebox.Type.String(),
324
- toolName: _sinclair_typebox.Type.String()
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) => client.checkToolPermission({
327
- platform: params.platform,
328
- senderId: params.senderId,
329
- toolName: params.toolName
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 claimed identity's phone (SMS) or email. The person must relay the phrase back to confirm they own that identity.",
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
- requestingPlatform: _sinclair_typebox.Type.String({ description: "Platform the requester is on (discord, slack, chat, etc.)" }),
339
- requestingPlatformId: _sinclair_typebox.Type.String({ description: "Requester's platform-specific user ID" }),
340
- preferredChannel: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "'sms' or 'email'" }))
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) => client.requestIdentityVerification({
343
- claimedIdentityId: params.claimedIdentityId,
344
- requestingIdentityId: params.requestingIdentityId,
345
- requestingPlatform: params.requestingPlatform,
346
- requestingPlatformId: params.requestingPlatformId,
347
- preferredChannel: params.preferredChannel
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 providing the three-word phrase. On success, the requesting identity is merged into the claimed identity.",
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 SMS or email" })
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 platform = ctx.channelId ?? "unknown";
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 ?? `${platform}:${senderId}`;
376
- const cached = getCached(cacheKey);
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
- 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,
370
+ const r = await client.resolveIdentity({
371
+ provider,
393
372
  platformId: senderId
394
373
  });
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 {
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} → ${resolveResult.identityId ?? "unknown"} (${resolveResult.status})`);
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
- if (failureMode === "closed") return {
393
+ return {
412
394
  block: true,
413
- blockReason: "Identity: identity service unavailable — access denied (closed mode)"
395
+ blockReason: "Identity: identity service unavailable — access denied (fail-closed)"
414
396
  };
415
397
  }
416
398
  }, { 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}`);
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
  };