@alfe.ai/openclaw-identity 0.0.14 → 0.0.16
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.d.cts +32 -8
- package/dist/index.d.ts +32 -8
- package/dist/plugin2.cjs +69 -13
- package/dist/plugin2.js +69 -13
- package/openclaw.plugin.json +16 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import plugin from "./plugin.cjs";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/runtime-contract.d.ts
|
|
4
4
|
|
|
5
|
-
type IdentityFailureMode = "open" | "closed" | "permissive";
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* OpenClaw runtime contract for the identity plugin's `before_tool_call` hook.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 Stage A.0 of the roles/permissions/sessions plan. The plugin's hook
|
|
9
|
+
* signature documents what the OpenClaw daemon must populate when a tool call
|
|
10
|
+
* is dispatched. The actual gate logic (Stage E) reads these fields to make a
|
|
11
|
+
* permit/deny decision via `@auriclabs/roles`.
|
|
12
|
+
*
|
|
13
|
+
* Contract:
|
|
14
|
+
* - `ToolCallEvent` describes the tool being called (name + args).
|
|
15
|
+
* - `ToolCallContext` describes the calling context (tenant, agent, actor).
|
|
16
|
+
*
|
|
17
|
+
* The hook receives `(event, ctx)`. Both arguments are required. The daemon
|
|
18
|
+
* is responsible for populating every required field; the plugin's gate
|
|
19
|
+
* applies the per-agent `IdentityFailureMode` when an optional field is
|
|
20
|
+
* absent and a permission decision can't be made cleanly.
|
|
21
|
+
*
|
|
22
|
+
* Round 6 note (gate context shape): the gate context passed to `sift` is
|
|
23
|
+
* `{ tool: event.toolName, args: event.toolArgs }` with strict namespacing
|
|
24
|
+
* (no spread). This prevents an attacker-controlled `tool` field inside
|
|
25
|
+
* `toolArgs` from overriding the gate's value.
|
|
9
26
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Per-agent fallback policy when an optional `ctx.actingIdentityId` is
|
|
29
|
+
* absent (e.g. autonomous tool_chain runs that don't carry a chat-side
|
|
30
|
+
* sender). Round-5 unified plan literals.
|
|
31
|
+
*
|
|
32
|
+
* - "open" — admit the call (legacy default).
|
|
33
|
+
* - "closed" — block the call (production default once Stage E ships).
|
|
34
|
+
* - "permissive" — admit with a warn-level audit log; intermediate during
|
|
35
|
+
* the Stage H dwell window.
|
|
36
|
+
*/
|
|
37
|
+
type IdentityFailureMode = "open" | "closed" | "permissive";
|
|
14
38
|
/**
|
|
15
39
|
* The tool the agent is about to call.
|
|
16
40
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import plugin from "./plugin.js";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/runtime-contract.d.ts
|
|
4
4
|
|
|
5
|
-
type IdentityFailureMode = "open" | "closed" | "permissive";
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* OpenClaw runtime contract for the identity plugin's `before_tool_call` hook.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 Stage A.0 of the roles/permissions/sessions plan. The plugin's hook
|
|
9
|
+
* signature documents what the OpenClaw daemon must populate when a tool call
|
|
10
|
+
* is dispatched. The actual gate logic (Stage E) reads these fields to make a
|
|
11
|
+
* permit/deny decision via `@auriclabs/roles`.
|
|
12
|
+
*
|
|
13
|
+
* Contract:
|
|
14
|
+
* - `ToolCallEvent` describes the tool being called (name + args).
|
|
15
|
+
* - `ToolCallContext` describes the calling context (tenant, agent, actor).
|
|
16
|
+
*
|
|
17
|
+
* The hook receives `(event, ctx)`. Both arguments are required. The daemon
|
|
18
|
+
* is responsible for populating every required field; the plugin's gate
|
|
19
|
+
* applies the per-agent `IdentityFailureMode` when an optional field is
|
|
20
|
+
* absent and a permission decision can't be made cleanly.
|
|
21
|
+
*
|
|
22
|
+
* Round 6 note (gate context shape): the gate context passed to `sift` is
|
|
23
|
+
* `{ tool: event.toolName, args: event.toolArgs }` with strict namespacing
|
|
24
|
+
* (no spread). This prevents an attacker-controlled `tool` field inside
|
|
25
|
+
* `toolArgs` from overriding the gate's value.
|
|
9
26
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Per-agent fallback policy when an optional `ctx.actingIdentityId` is
|
|
29
|
+
* absent (e.g. autonomous tool_chain runs that don't carry a chat-side
|
|
30
|
+
* sender). Round-5 unified plan literals.
|
|
31
|
+
*
|
|
32
|
+
* - "open" — admit the call (legacy default).
|
|
33
|
+
* - "closed" — block the call (production default once Stage E ships).
|
|
34
|
+
* - "permissive" — admit with a warn-level audit log; intermediate during
|
|
35
|
+
* the Stage H dwell window.
|
|
36
|
+
*/
|
|
37
|
+
type IdentityFailureMode = "open" | "closed" | "permissive";
|
|
14
38
|
/**
|
|
15
39
|
* The tool the agent is about to call.
|
|
16
40
|
*
|
package/dist/plugin2.cjs
CHANGED
|
@@ -82,21 +82,49 @@ function resolveToolGatingMode() {
|
|
|
82
82
|
if (raw === "fail-open" || raw === "fail-closed") return raw;
|
|
83
83
|
return "fail-open";
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate `agent:chat` — the inbound conversational gate (Layer 2 of the
|
|
87
|
+
* default-deny chat path). Mirrors `evaluateAgentExec` but with no
|
|
88
|
+
* tool/args context — chat admission only depends on whether the
|
|
89
|
+
* resolved identity holds `agent:chat` at the agent's scope.
|
|
90
|
+
*
|
|
91
|
+
* Layer 1 (`services/chat`) already gates this for chat WS + channel
|
|
92
|
+
* adapter inject. The OpenClaw plugin's `message_received` hook is
|
|
93
|
+
* defense-in-depth — it catches any path that reaches the agent
|
|
94
|
+
* runtime without going through Layer 1.
|
|
95
|
+
*/
|
|
96
|
+
function evaluateAgentChat(permissions, args) {
|
|
97
|
+
if (!args.agentId) return { allowed: false };
|
|
98
|
+
const ability = (0, _auriclabs_roles.createAbility)(permissions);
|
|
99
|
+
const gatePermission = {
|
|
100
|
+
subject: "agent",
|
|
101
|
+
action: "chat",
|
|
102
|
+
scope: `agent:${args.agentId}`
|
|
103
|
+
};
|
|
104
|
+
return { allowed: ability.has(gatePermission, {}) };
|
|
105
|
+
}
|
|
85
106
|
//#endregion
|
|
86
107
|
//#region src/plugin.ts
|
|
87
108
|
/**
|
|
88
109
|
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
89
110
|
*
|
|
90
|
-
* HTTP-based identity resolution +
|
|
111
|
+
* HTTP-based identity resolution + role-based admission gate. Installed as
|
|
91
112
|
* part of the core alfe integration on every agent.
|
|
92
113
|
*
|
|
93
114
|
* Hooks:
|
|
94
|
-
* - message_received →
|
|
95
|
-
*
|
|
115
|
+
* - message_received → resolves the sender via the agent API and gates
|
|
116
|
+
* on `agent:chat` at the agent's scope using the
|
|
117
|
+
* resolved identity's permission set
|
|
118
|
+
* (`evaluateAgentChat`). Blocks unknown senders
|
|
119
|
+
* and any identity without the permission. Layer 2
|
|
120
|
+
* of the default-deny chat path.
|
|
96
121
|
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
97
|
-
* hook (Phase 1 Stage A.0).
|
|
98
|
-
*
|
|
99
|
-
*
|
|
122
|
+
* hook (Phase 1 Stage A.0). Gates `agent:exec`
|
|
123
|
+
* via `evaluateAgentExec` against the token's
|
|
124
|
+
* own permissions (token path) or the cached
|
|
125
|
+
* resolved permissions (JWT path). Sift
|
|
126
|
+
* conditions on the permission narrow which
|
|
127
|
+
* tools the call may invoke.
|
|
100
128
|
*
|
|
101
129
|
* `after_tool_call` is intentionally absent — tool-call audit lives in a
|
|
102
130
|
* future dedicated audit service, not in identity.
|
|
@@ -353,6 +381,15 @@ const plugin = {
|
|
|
353
381
|
const provider = ctx.channelId ?? "unknown";
|
|
354
382
|
const senderId = event.metadata?.UserId ?? event.from;
|
|
355
383
|
if (!senderId) return;
|
|
384
|
+
const agentId = ctx.agentId;
|
|
385
|
+
if (!agentId) {
|
|
386
|
+
log.warn(`message_received without ctx.agentId — fail-closed`);
|
|
387
|
+
return {
|
|
388
|
+
block: true,
|
|
389
|
+
blockReason: "Identity: agentId missing on message context"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const forwarded = Array.isArray(event.metadata?.SenderPermissions) ? event.metadata.SenderPermissions.filter((p) => typeof p === "string") : [];
|
|
356
393
|
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
357
394
|
const cached = getCachedResolve(cacheKey);
|
|
358
395
|
if (cached) {
|
|
@@ -360,9 +397,9 @@ const plugin = {
|
|
|
360
397
|
block: true,
|
|
361
398
|
blockReason: "Identity: identity not provisioned for this channel"
|
|
362
399
|
};
|
|
363
|
-
if (!cached.
|
|
400
|
+
if (!evaluateAgentChat([...cached.permissions, ...forwarded], { agentId }).allowed) return {
|
|
364
401
|
block: true,
|
|
365
|
-
blockReason: "Identity:
|
|
402
|
+
blockReason: "Identity: chat not permitted for this sender"
|
|
366
403
|
};
|
|
367
404
|
return;
|
|
368
405
|
}
|
|
@@ -373,8 +410,8 @@ const plugin = {
|
|
|
373
410
|
});
|
|
374
411
|
setCachedResolve(cacheKey, {
|
|
375
412
|
identityId: r.identityId,
|
|
376
|
-
|
|
377
|
-
|
|
413
|
+
status: r.status,
|
|
414
|
+
permissions: [...r.permissions, ...forwarded]
|
|
378
415
|
});
|
|
379
416
|
if (r.identityId == null) {
|
|
380
417
|
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
@@ -383,9 +420,10 @@ const plugin = {
|
|
|
383
420
|
blockReason: "Identity: identity not provisioned for this channel"
|
|
384
421
|
};
|
|
385
422
|
}
|
|
386
|
-
|
|
423
|
+
const cachedAfterFill = getCachedResolve(cacheKey);
|
|
424
|
+
if (!evaluateAgentChat(cachedAfterFill ? [...cachedAfterFill.permissions] : [...r.permissions, ...forwarded], { agentId }).allowed) return {
|
|
387
425
|
block: true,
|
|
388
|
-
blockReason: "Identity:
|
|
426
|
+
blockReason: "Identity: chat not permitted for this sender"
|
|
389
427
|
};
|
|
390
428
|
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
391
429
|
} catch (e) {
|
|
@@ -405,7 +443,25 @@ const plugin = {
|
|
|
405
443
|
}), resolveToolGatingMode(), event.toolName);
|
|
406
444
|
return Promise.resolve(blocked);
|
|
407
445
|
}
|
|
408
|
-
|
|
446
|
+
const cacheKey = ctx.conversationId ?? (ctx.channelId && ctx.actingIdentityId ? `${ctx.channelId}:${ctx.actingIdentityId}` : null);
|
|
447
|
+
if (cacheKey) {
|
|
448
|
+
const cached = getCachedResolve(cacheKey);
|
|
449
|
+
if (cached && cached.permissions.length > 0) {
|
|
450
|
+
const blocked = applyGateMode(evaluateAgentExec([...cached.permissions], {
|
|
451
|
+
agentId: ctx.agentId,
|
|
452
|
+
toolName: event.toolName,
|
|
453
|
+
toolArgs: event.toolArgs
|
|
454
|
+
}), resolveToolGatingMode(), event.toolName);
|
|
455
|
+
return Promise.resolve(blocked);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const fallback = evaluateAgentExec([], {
|
|
459
|
+
agentId: ctx.agentId,
|
|
460
|
+
toolName: event.toolName,
|
|
461
|
+
toolArgs: event.toolArgs
|
|
462
|
+
});
|
|
463
|
+
const mode = resolveToolGatingMode();
|
|
464
|
+
return Promise.resolve(applyGateMode(fallback, mode, event.toolName));
|
|
409
465
|
};
|
|
410
466
|
api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
|
|
411
467
|
log.info("Alfe Identity plugin activated");
|
package/dist/plugin2.js
CHANGED
|
@@ -82,21 +82,49 @@ function resolveToolGatingMode() {
|
|
|
82
82
|
if (raw === "fail-open" || raw === "fail-closed") return raw;
|
|
83
83
|
return "fail-open";
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate `agent:chat` — the inbound conversational gate (Layer 2 of the
|
|
87
|
+
* default-deny chat path). Mirrors `evaluateAgentExec` but with no
|
|
88
|
+
* tool/args context — chat admission only depends on whether the
|
|
89
|
+
* resolved identity holds `agent:chat` at the agent's scope.
|
|
90
|
+
*
|
|
91
|
+
* Layer 1 (`services/chat`) already gates this for chat WS + channel
|
|
92
|
+
* adapter inject. The OpenClaw plugin's `message_received` hook is
|
|
93
|
+
* defense-in-depth — it catches any path that reaches the agent
|
|
94
|
+
* runtime without going through Layer 1.
|
|
95
|
+
*/
|
|
96
|
+
function evaluateAgentChat(permissions, args) {
|
|
97
|
+
if (!args.agentId) return { allowed: false };
|
|
98
|
+
const ability = createAbility(permissions);
|
|
99
|
+
const gatePermission = {
|
|
100
|
+
subject: "agent",
|
|
101
|
+
action: "chat",
|
|
102
|
+
scope: `agent:${args.agentId}`
|
|
103
|
+
};
|
|
104
|
+
return { allowed: ability.has(gatePermission, {}) };
|
|
105
|
+
}
|
|
85
106
|
//#endregion
|
|
86
107
|
//#region src/plugin.ts
|
|
87
108
|
/**
|
|
88
109
|
* @alfe.ai/openclaw-identity — OpenClaw native plugin
|
|
89
110
|
*
|
|
90
|
-
* HTTP-based identity resolution +
|
|
111
|
+
* HTTP-based identity resolution + role-based admission gate. Installed as
|
|
91
112
|
* part of the core alfe integration on every agent.
|
|
92
113
|
*
|
|
93
114
|
* Hooks:
|
|
94
|
-
* - message_received →
|
|
95
|
-
*
|
|
115
|
+
* - message_received → resolves the sender via the agent API and gates
|
|
116
|
+
* on `agent:chat` at the agent's scope using the
|
|
117
|
+
* resolved identity's permission set
|
|
118
|
+
* (`evaluateAgentChat`). Blocks unknown senders
|
|
119
|
+
* and any identity without the permission. Layer 2
|
|
120
|
+
* of the default-deny chat path.
|
|
96
121
|
* - before_tool_call → typed `(event: ToolCallEvent, ctx: ToolCallContext)`
|
|
97
|
-
* hook (Phase 1 Stage A.0).
|
|
98
|
-
*
|
|
99
|
-
*
|
|
122
|
+
* hook (Phase 1 Stage A.0). Gates `agent:exec`
|
|
123
|
+
* via `evaluateAgentExec` against the token's
|
|
124
|
+
* own permissions (token path) or the cached
|
|
125
|
+
* resolved permissions (JWT path). Sift
|
|
126
|
+
* conditions on the permission narrow which
|
|
127
|
+
* tools the call may invoke.
|
|
100
128
|
*
|
|
101
129
|
* `after_tool_call` is intentionally absent — tool-call audit lives in a
|
|
102
130
|
* future dedicated audit service, not in identity.
|
|
@@ -353,6 +381,15 @@ const plugin = {
|
|
|
353
381
|
const provider = ctx.channelId ?? "unknown";
|
|
354
382
|
const senderId = event.metadata?.UserId ?? event.from;
|
|
355
383
|
if (!senderId) return;
|
|
384
|
+
const agentId = ctx.agentId;
|
|
385
|
+
if (!agentId) {
|
|
386
|
+
log.warn(`message_received without ctx.agentId — fail-closed`);
|
|
387
|
+
return {
|
|
388
|
+
block: true,
|
|
389
|
+
blockReason: "Identity: agentId missing on message context"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const forwarded = Array.isArray(event.metadata?.SenderPermissions) ? event.metadata.SenderPermissions.filter((p) => typeof p === "string") : [];
|
|
356
393
|
const cacheKey = ctx.conversationId ?? `${provider}:${senderId}`;
|
|
357
394
|
const cached = getCachedResolve(cacheKey);
|
|
358
395
|
if (cached) {
|
|
@@ -360,9 +397,9 @@ const plugin = {
|
|
|
360
397
|
block: true,
|
|
361
398
|
blockReason: "Identity: identity not provisioned for this channel"
|
|
362
399
|
};
|
|
363
|
-
if (!cached.
|
|
400
|
+
if (!evaluateAgentChat([...cached.permissions, ...forwarded], { agentId }).allowed) return {
|
|
364
401
|
block: true,
|
|
365
|
-
blockReason: "Identity:
|
|
402
|
+
blockReason: "Identity: chat not permitted for this sender"
|
|
366
403
|
};
|
|
367
404
|
return;
|
|
368
405
|
}
|
|
@@ -373,8 +410,8 @@ const plugin = {
|
|
|
373
410
|
});
|
|
374
411
|
setCachedResolve(cacheKey, {
|
|
375
412
|
identityId: r.identityId,
|
|
376
|
-
|
|
377
|
-
|
|
413
|
+
status: r.status,
|
|
414
|
+
permissions: [...r.permissions, ...forwarded]
|
|
378
415
|
});
|
|
379
416
|
if (r.identityId == null) {
|
|
380
417
|
log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
|
|
@@ -383,9 +420,10 @@ const plugin = {
|
|
|
383
420
|
blockReason: "Identity: identity not provisioned for this channel"
|
|
384
421
|
};
|
|
385
422
|
}
|
|
386
|
-
|
|
423
|
+
const cachedAfterFill = getCachedResolve(cacheKey);
|
|
424
|
+
if (!evaluateAgentChat(cachedAfterFill ? [...cachedAfterFill.permissions] : [...r.permissions, ...forwarded], { agentId }).allowed) return {
|
|
387
425
|
block: true,
|
|
388
|
-
blockReason: "Identity:
|
|
426
|
+
blockReason: "Identity: chat not permitted for this sender"
|
|
389
427
|
};
|
|
390
428
|
log.info(`Identity resolved: ${senderId} → ${r.identityId} (${r.status})`);
|
|
391
429
|
} catch (e) {
|
|
@@ -405,7 +443,25 @@ const plugin = {
|
|
|
405
443
|
}), resolveToolGatingMode(), event.toolName);
|
|
406
444
|
return Promise.resolve(blocked);
|
|
407
445
|
}
|
|
408
|
-
|
|
446
|
+
const cacheKey = ctx.conversationId ?? (ctx.channelId && ctx.actingIdentityId ? `${ctx.channelId}:${ctx.actingIdentityId}` : null);
|
|
447
|
+
if (cacheKey) {
|
|
448
|
+
const cached = getCachedResolve(cacheKey);
|
|
449
|
+
if (cached && cached.permissions.length > 0) {
|
|
450
|
+
const blocked = applyGateMode(evaluateAgentExec([...cached.permissions], {
|
|
451
|
+
agentId: ctx.agentId,
|
|
452
|
+
toolName: event.toolName,
|
|
453
|
+
toolArgs: event.toolArgs
|
|
454
|
+
}), resolveToolGatingMode(), event.toolName);
|
|
455
|
+
return Promise.resolve(blocked);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const fallback = evaluateAgentExec([], {
|
|
459
|
+
agentId: ctx.agentId,
|
|
460
|
+
toolName: event.toolName,
|
|
461
|
+
toolArgs: event.toolArgs
|
|
462
|
+
});
|
|
463
|
+
const mode = resolveToolGatingMode();
|
|
464
|
+
return Promise.resolve(applyGateMode(fallback, mode, event.toolName));
|
|
409
465
|
};
|
|
410
466
|
api.on("before_tool_call", (...args) => beforeToolCallStub(args[0], args[1]), { priority: 100 });
|
|
411
467
|
log.info("Alfe Identity plugin activated");
|
package/openclaw.plugin.json
CHANGED
|
@@ -7,5 +7,21 @@
|
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"properties": {}
|
|
10
|
+
},
|
|
11
|
+
"activation": { "onStartup": true },
|
|
12
|
+
"contracts": {
|
|
13
|
+
"tools": [
|
|
14
|
+
"who_is_this",
|
|
15
|
+
"lookup_identity",
|
|
16
|
+
"tag_identity",
|
|
17
|
+
"add_identity_note",
|
|
18
|
+
"update_identity",
|
|
19
|
+
"merge_identities",
|
|
20
|
+
"unmerge_identities",
|
|
21
|
+
"request_identity_verification",
|
|
22
|
+
"confirm_identity_verification",
|
|
23
|
+
"get_identity_changelog",
|
|
24
|
+
"rollback_identity"
|
|
25
|
+
]
|
|
10
26
|
}
|
|
11
27
|
}
|
package/package.json
CHANGED